Skip to content
This repository has been archived by the owner on Nov 2, 2022. It is now read-only.

Commit

Permalink
Register formula dependencies and listen for chnge
Browse files Browse the repository at this point in the history
  • Loading branch information
Vic van Cooten committed Sep 30, 2021
1 parent 4340d2e commit 04b4a05
Show file tree
Hide file tree
Showing 5 changed files with 282 additions and 8 deletions.
30 changes: 30 additions & 0 deletions src/appbox-formulas/Functions/and.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Formula from "../index";

/*
* AND(...arg1, etc)
* returns true if ALL children are true
*/

export default {
execute: (fArguments, data, formula: Formula) =>
new Promise(async (resolve, reject) => {
let isTrue = true;
for (let x = 0; x < fArguments.length; x++) {
if (fArguments[x] !== true) isTrue = false;
}

resolve(isTrue);
}),
onCompile: (fArguments) => {
const deps = [];

// For each argument, check if their type is string (so not a "string", but a string). If so, it refers to a field and is a dependency.
for (let x = 0; x < fArguments.length; x++) {
if (typeof fArguments[x] === "string") deps.push(fArguments[x]);
}

return deps;
},
// Give a sample result so it's parent functions know what we will return on execute and can perform the right precompiling.
returnPreview: true,
};
7 changes: 7 additions & 0 deletions src/appbox-formulas/Functions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import and from "./and";

const functions = {
and,
};

export default functions;
93 changes: 93 additions & 0 deletions src/appbox-formulas/Types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { Collection } from "mongodb";

/* Server technical types */
export interface DBCollectionsType {
models: Collection;
objects: Collection;
usersettings: Collection;
}

/* Model */
export interface ModelType {
_id: string;
key: string;
key_plural: string;
label: string;
label_plural: string;
app: string;
primary: string;
icon: string;
locked?: boolean;
fields: { [key: string]: ModelFieldType };
layouts: { [key: string]: ModelLayoutType };
lists: { [key: string]: ModelListType };
permissions: {
create: string[];
read: string[];
read_own: string[];
update: string[];
update_own: string[];
delete: string[];
delete_own: string[];
};
}

// Field
export interface ModelFieldType {
label: string;
type?: "text" | "number" | "relationship" | "formula" | "options";
required?: boolean;
unique?: boolean;
// Options
selectMultiple?: boolean;
optionsDisplayAs?: "dropdown" | "list" | string;
options?: { label: string; value: string }[];
// Relationship
relationshipTo?: string;
// Formula
formula?: string;
}

// Layout
export interface ModelLayoutType {
label: string;
layout: LayoutItemType[];
}
export interface LayoutItemType {
key?: string;
label: string;
type: string;
items?: LayoutItemType[];
args?: { [key: string]: any };
}

// List
export interface ModelListType {
label?: string;
filter?: {};
fields?: string[];
}

/* Object types */
export interface ObjectType {
_id: string;
meta: {
model: string;
createdOn: Date;
lastModifiedOn: Date;
createdBy: string;
lastModifiedBy: string;
owner: string;
team?: string;
};
[key: string]: any;
}

export interface UserObjectType extends ObjectType {
username: string;
first_name: string;
last_name: string;
email: string;
password: string;
roles: string[];
}
121 changes: 117 additions & 4 deletions src/appbox-formulas/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,39 @@
import { ModelType } from "./Types";
import { find } from "lodash";
import uniqid from "uniqid";
const uniqid = require("uniqid");

/*
* The Formula class
*/
class Formula {
id = uniqid();
// Label
label;
// Original formula string
formulaString;
// Holds formula template (tags replaced by unique identifiers)
formulaTemplate;
// Array holding all tags
tags: { tag: string; identifier: string }[] = [];
// Array holding all dependencies
dependencies: { field: string; model: string; localDependency?: true }[] = [];
// Hold all models
models: ModelType[];
// Promise to check if constructor is done working asynchronously
onParsed: Promise<void>;

// Constructor
constructor(formula, mode: "{{" | "[[" = "{{") {
constructor(
formula,
startingModelKey: string,
label?: string,
mode: "{{" | "[[" = "{{", // This is for nested formulas, such as templates
models?: ModelType[] // Some data may be statically delivered in JSON format. Then we don't need this. If we have dynamic (field__r) data we need to query the database and parse the correct dependencies.
) {
this.formulaString = formula;
this.formulaTemplate = formula;
this.models = models;
this.label = label;

// Pre-parse tags
const tagPattern =
Expand All @@ -27,9 +49,100 @@ class Formula {
);
});

// Loop through all the tags
console.log(this.formulaString, this.formulaTemplate, this.tags);
// Parse dependencies
this.onParsed = new Promise((resolve, reject) =>
this.parseDependencies(startingModelKey).then(
() => resolve(),
(reason) =>
reject(`(${label}) couldn't process dependencies: ${reason}`)
)
);
}

// Parse dependencies for all tags (asynchronously used in )
parseDependencies = (startModelKey: string) =>
new Promise<void>(async (resolve, reject) => {
//@ts-ignore
await this.tags.reduce(async (prevTag, tag) => {
await prevTag;

const tagParts = tag.tag.split(/[-+*\/](?![^\(]*\))/gm);
//@ts-ignore
await tagParts.reduce(async (prevTagPart, tagPart) => {
// The regexp splits on -, but not within parenthesis
const part = tagPart.trim();

// Check the context of the tag part and perform the appropriate action
if (part.match(/\w*\(.+\)/)) {
// This part has a function call. We need to preprocess these functions to figure out what the dependencies are.
const func = new RegExp(/(?<fName>\w*)\((?<fArgs>.*)\)/gm).exec(
part
);
console.log("Preprocessing", func.groups.fName, func.groups.fArgs);
} else if (part.match(/\./)) {
if (part.match("__r")) {
// This is an object based relationship. Resolve the dependencies
if (this.models) {
// We're going to split by . and resolve them all to set a dependency.
const tagParts = part.split(".");
let currentModelKey = startModelKey;
//@ts-ignore
await tagParts.reduce(async (prevPart, currPart) => {
await prevPart;

if (currPart.match("__r")) {
const fieldName = currPart.replace("__r", "");

// This is a part of the relationship. It needs to be registered as dependency, in case it's value changes.
this.dependencies.push({
model: currentModelKey,
field: fieldName,
...(currentModelKey === startModelKey
? { localDependency: true }
: {}),
});
// It also needs to be parsed to figure out what model the next
const currentModel = find(
this.models,
(o) => o.key === currentModelKey
);
const field = currentModel.fields[fieldName];
currentModelKey = field.relationshipTo;
} else {
this.dependencies.push({
model: currentModelKey,
field: currPart,
});
}

return currPart;
}, tagParts[0]);
resolve();
} else {
reject("no-models-provided");
}
} else {
// This is a regular dependency (a.b.c), so we can just add it as a field
this.dependencies.push({
field: part,
model: startModelKey,
localDependency: true,
});
}
} else {
this.dependencies.push({
field: part,
model: startModelKey,
localDependency: true,
});
}
}, tagParts[0]);

return tag;
}, this.tags[0]);

resolve();
});
}

export default Formula;
39 changes: 35 additions & 4 deletions src/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ class Engine {
objects: null,
usersettings: null,
};
// All models
models: ModelType[];
// Update triggers
updateTriggers: { [key: string]: string } = {};

// Initialise
constructor(db) {
Expand All @@ -23,19 +27,46 @@ class Engine {
objects: db.collection("objects"),
usersettings: db.collection("usersettings"),
};
this.collections.models
.find({})
.toArray()
.then((models: ModelType[]) => (this.models = models));
console.log("Engine booting up");
}

// Parse formulas
parseFormulas() {
async parseFormulas() {
console.log("Parsing formulas");
this.collections.models.find({}).forEach((model: ModelType) => {
const models = await this.collections.models.find({}).toArray();
await models.reduce(async (prevModel, model) => {
await prevModel;

map(model.fields, (field, key) => {
if (field.type === "formula") {
console.log(`🧪 Parsing formula ${field.label}.`);
const formula = new Formula(field.formula);
console.log(`🧪 Parsing formula '${field.label}'.`);
const formula = new Formula(
field.formula,
model.key,
`🧪 ${field.label}`,
"{{",
this.models
);
formula.onParsed.then(() => {
formula.dependencies.map((dep) => {
this.updateTriggers[`${dep.model}___${dep.field}`] = formula.id;
});
});
}
});
return model;
}, models[0]);
console.log("🥼 Done parsing formulas");
this.registerOnObjectChangeListeners();
}

registerOnObjectChangeListeners() {
this.collections.objects.watch().on("change", async (change) => {
console.log(change);
});
}
}
Expand Down

0 comments on commit 04b4a05

Please sign in to comment.