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

Commit 04b4a05

Browse files
author
Vic van Cooten
committed
Register formula dependencies and listen for chnge
1 parent 4340d2e commit 04b4a05

File tree

5 files changed

+282
-8
lines changed

5 files changed

+282
-8
lines changed

src/appbox-formulas/Functions/and.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import Formula from "../index";
2+
3+
/*
4+
* AND(...arg1, etc)
5+
* returns true if ALL children are true
6+
*/
7+
8+
export default {
9+
execute: (fArguments, data, formula: Formula) =>
10+
new Promise(async (resolve, reject) => {
11+
let isTrue = true;
12+
for (let x = 0; x < fArguments.length; x++) {
13+
if (fArguments[x] !== true) isTrue = false;
14+
}
15+
16+
resolve(isTrue);
17+
}),
18+
onCompile: (fArguments) => {
19+
const deps = [];
20+
21+
// 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.
22+
for (let x = 0; x < fArguments.length; x++) {
23+
if (typeof fArguments[x] === "string") deps.push(fArguments[x]);
24+
}
25+
26+
return deps;
27+
},
28+
// Give a sample result so it's parent functions know what we will return on execute and can perform the right precompiling.
29+
returnPreview: true,
30+
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import and from "./and";
2+
3+
const functions = {
4+
and,
5+
};
6+
7+
export default functions;

src/appbox-formulas/Types.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { Collection } from "mongodb";
2+
3+
/* Server technical types */
4+
export interface DBCollectionsType {
5+
models: Collection;
6+
objects: Collection;
7+
usersettings: Collection;
8+
}
9+
10+
/* Model */
11+
export interface ModelType {
12+
_id: string;
13+
key: string;
14+
key_plural: string;
15+
label: string;
16+
label_plural: string;
17+
app: string;
18+
primary: string;
19+
icon: string;
20+
locked?: boolean;
21+
fields: { [key: string]: ModelFieldType };
22+
layouts: { [key: string]: ModelLayoutType };
23+
lists: { [key: string]: ModelListType };
24+
permissions: {
25+
create: string[];
26+
read: string[];
27+
read_own: string[];
28+
update: string[];
29+
update_own: string[];
30+
delete: string[];
31+
delete_own: string[];
32+
};
33+
}
34+
35+
// Field
36+
export interface ModelFieldType {
37+
label: string;
38+
type?: "text" | "number" | "relationship" | "formula" | "options";
39+
required?: boolean;
40+
unique?: boolean;
41+
// Options
42+
selectMultiple?: boolean;
43+
optionsDisplayAs?: "dropdown" | "list" | string;
44+
options?: { label: string; value: string }[];
45+
// Relationship
46+
relationshipTo?: string;
47+
// Formula
48+
formula?: string;
49+
}
50+
51+
// Layout
52+
export interface ModelLayoutType {
53+
label: string;
54+
layout: LayoutItemType[];
55+
}
56+
export interface LayoutItemType {
57+
key?: string;
58+
label: string;
59+
type: string;
60+
items?: LayoutItemType[];
61+
args?: { [key: string]: any };
62+
}
63+
64+
// List
65+
export interface ModelListType {
66+
label?: string;
67+
filter?: {};
68+
fields?: string[];
69+
}
70+
71+
/* Object types */
72+
export interface ObjectType {
73+
_id: string;
74+
meta: {
75+
model: string;
76+
createdOn: Date;
77+
lastModifiedOn: Date;
78+
createdBy: string;
79+
lastModifiedBy: string;
80+
owner: string;
81+
team?: string;
82+
};
83+
[key: string]: any;
84+
}
85+
86+
export interface UserObjectType extends ObjectType {
87+
username: string;
88+
first_name: string;
89+
last_name: string;
90+
email: string;
91+
password: string;
92+
roles: string[];
93+
}

src/appbox-formulas/index.ts

Lines changed: 117 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,39 @@
1+
import { ModelType } from "./Types";
2+
import { find } from "lodash";
3+
import uniqid from "uniqid";
14
const uniqid = require("uniqid");
2-
5+
/*
6+
* The Formula class
7+
*/
38
class Formula {
9+
id = uniqid();
10+
// Label
11+
label;
412
// Original formula string
513
formulaString;
614
// Holds formula template (tags replaced by unique identifiers)
715
formulaTemplate;
816
// Array holding all tags
917
tags: { tag: string; identifier: string }[] = [];
18+
// Array holding all dependencies
19+
dependencies: { field: string; model: string; localDependency?: true }[] = [];
20+
// Hold all models
21+
models: ModelType[];
22+
// Promise to check if constructor is done working asynchronously
23+
onParsed: Promise<void>;
1024

1125
// Constructor
12-
constructor(formula, mode: "{{" | "[[" = "{{") {
26+
constructor(
27+
formula,
28+
startingModelKey: string,
29+
label?: string,
30+
mode: "{{" | "[[" = "{{", // This is for nested formulas, such as templates
31+
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.
32+
) {
1333
this.formulaString = formula;
1434
this.formulaTemplate = formula;
35+
this.models = models;
36+
this.label = label;
1537

1638
// Pre-parse tags
1739
const tagPattern =
@@ -27,9 +49,100 @@ class Formula {
2749
);
2850
});
2951

30-
// Loop through all the tags
31-
console.log(this.formulaString, this.formulaTemplate, this.tags);
52+
// Parse dependencies
53+
this.onParsed = new Promise((resolve, reject) =>
54+
this.parseDependencies(startingModelKey).then(
55+
() => resolve(),
56+
(reason) =>
57+
reject(`(${label}) couldn't process dependencies: ${reason}`)
58+
)
59+
);
3260
}
61+
62+
// Parse dependencies for all tags (asynchronously used in )
63+
parseDependencies = (startModelKey: string) =>
64+
new Promise<void>(async (resolve, reject) => {
65+
//@ts-ignore
66+
await this.tags.reduce(async (prevTag, tag) => {
67+
await prevTag;
68+
69+
const tagParts = tag.tag.split(/[-+*\/](?![^\(]*\))/gm);
70+
//@ts-ignore
71+
await tagParts.reduce(async (prevTagPart, tagPart) => {
72+
// The regexp splits on -, but not within parenthesis
73+
const part = tagPart.trim();
74+
75+
// Check the context of the tag part and perform the appropriate action
76+
if (part.match(/\w*\(.+\)/)) {
77+
// This part has a function call. We need to preprocess these functions to figure out what the dependencies are.
78+
const func = new RegExp(/(?<fName>\w*)\((?<fArgs>.*)\)/gm).exec(
79+
part
80+
);
81+
console.log("Preprocessing", func.groups.fName, func.groups.fArgs);
82+
} else if (part.match(/\./)) {
83+
if (part.match("__r")) {
84+
// This is an object based relationship. Resolve the dependencies
85+
if (this.models) {
86+
// We're going to split by . and resolve them all to set a dependency.
87+
const tagParts = part.split(".");
88+
let currentModelKey = startModelKey;
89+
//@ts-ignore
90+
await tagParts.reduce(async (prevPart, currPart) => {
91+
await prevPart;
92+
93+
if (currPart.match("__r")) {
94+
const fieldName = currPart.replace("__r", "");
95+
96+
// This is a part of the relationship. It needs to be registered as dependency, in case it's value changes.
97+
this.dependencies.push({
98+
model: currentModelKey,
99+
field: fieldName,
100+
...(currentModelKey === startModelKey
101+
? { localDependency: true }
102+
: {}),
103+
});
104+
// It also needs to be parsed to figure out what model the next
105+
const currentModel = find(
106+
this.models,
107+
(o) => o.key === currentModelKey
108+
);
109+
const field = currentModel.fields[fieldName];
110+
currentModelKey = field.relationshipTo;
111+
} else {
112+
this.dependencies.push({
113+
model: currentModelKey,
114+
field: currPart,
115+
});
116+
}
117+
118+
return currPart;
119+
}, tagParts[0]);
120+
resolve();
121+
} else {
122+
reject("no-models-provided");
123+
}
124+
} else {
125+
// This is a regular dependency (a.b.c), so we can just add it as a field
126+
this.dependencies.push({
127+
field: part,
128+
model: startModelKey,
129+
localDependency: true,
130+
});
131+
}
132+
} else {
133+
this.dependencies.push({
134+
field: part,
135+
model: startModelKey,
136+
localDependency: true,
137+
});
138+
}
139+
}, tagParts[0]);
140+
141+
return tag;
142+
}, this.tags[0]);
143+
144+
resolve();
145+
});
33146
}
34147

35148
export default Formula;

src/engine.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ class Engine {
1414
objects: null,
1515
usersettings: null,
1616
};
17+
// All models
18+
models: ModelType[];
19+
// Update triggers
20+
updateTriggers: { [key: string]: string } = {};
1721

1822
// Initialise
1923
constructor(db) {
@@ -23,19 +27,46 @@ class Engine {
2327
objects: db.collection("objects"),
2428
usersettings: db.collection("usersettings"),
2529
};
30+
this.collections.models
31+
.find({})
32+
.toArray()
33+
.then((models: ModelType[]) => (this.models = models));
2634
console.log("Engine booting up");
2735
}
2836

2937
// Parse formulas
30-
parseFormulas() {
38+
async parseFormulas() {
3139
console.log("Parsing formulas");
32-
this.collections.models.find({}).forEach((model: ModelType) => {
40+
const models = await this.collections.models.find({}).toArray();
41+
await models.reduce(async (prevModel, model) => {
42+
await prevModel;
43+
3344
map(model.fields, (field, key) => {
3445
if (field.type === "formula") {
35-
console.log(`🧪 Parsing formula ${field.label}.`);
36-
const formula = new Formula(field.formula);
46+
console.log(`🧪 Parsing formula '${field.label}'.`);
47+
const formula = new Formula(
48+
field.formula,
49+
model.key,
50+
`🧪 ${field.label}`,
51+
"{{",
52+
this.models
53+
);
54+
formula.onParsed.then(() => {
55+
formula.dependencies.map((dep) => {
56+
this.updateTriggers[`${dep.model}___${dep.field}`] = formula.id;
57+
});
58+
});
3759
}
3860
});
61+
return model;
62+
}, models[0]);
63+
console.log("🥼 Done parsing formulas");
64+
this.registerOnObjectChangeListeners();
65+
}
66+
67+
registerOnObjectChangeListeners() {
68+
this.collections.objects.watch().on("change", async (change) => {
69+
console.log(change);
3970
});
4071
}
4172
}

0 commit comments

Comments
 (0)