@ai-matrx/records 0.82.0 → 0.83.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -1
- package/dist/memory/index.cjs +2 -0
- package/dist/memory/index.cjs.map +1 -1
- package/dist/memory/index.js +2 -0
- package/dist/memory/index.js.map +1 -1
- package/dist/templates/index.cjs +428 -139
- package/dist/templates/index.cjs.map +1 -1
- package/dist/templates/index.d.cts +9 -5
- package/dist/templates/index.d.ts +9 -5
- package/dist/templates/index.js +428 -139
- package/dist/templates/index.js.map +1 -1
- package/package.json +4 -4
package/dist/templates/index.cjs
CHANGED
|
@@ -30,7 +30,7 @@ __export(templates_exports, {
|
|
|
30
30
|
DEFAULT_TEMPLATE_AGENT: () => DEFAULT_TEMPLATE_AGENT,
|
|
31
31
|
DESIGNATED_REAL_NUMBERS: () => DESIGNATED_REAL_NUMBERS,
|
|
32
32
|
ENTITY_REFERENCE_KINDS: () => ENTITY_REFERENCE_KINDS,
|
|
33
|
-
|
|
33
|
+
FORMULA_FUNCTION_NAMES: () => FORMULA_FUNCTION_NAMES,
|
|
34
34
|
GALLERY_ONLY: () => GALLERY_ONLY,
|
|
35
35
|
GOOD_OUTCOME: () => GOOD_OUTCOME,
|
|
36
36
|
INDUSTRY_GROUPS: () => INDUSTRY_GROUPS,
|
|
@@ -113,7 +113,7 @@ __export(templates_exports, {
|
|
|
113
113
|
numberedStreet: () => numberedStreet,
|
|
114
114
|
packageDescribeResult: () => packageDescribeResult,
|
|
115
115
|
parseAt: () => parseAt,
|
|
116
|
-
|
|
116
|
+
parseGateFormula: () => parseGateFormula,
|
|
117
117
|
parseNumeric: () => parseNumeric,
|
|
118
118
|
pathOfAt: () => pathOfAt,
|
|
119
119
|
peopleOf: () => peopleOf,
|
|
@@ -492,6 +492,7 @@ var CATALOGUE_STATUSES = ["planned", "authoring", "checking", "waiting_on_lane_1
|
|
|
492
492
|
|
|
493
493
|
// src/templates/validate.ts
|
|
494
494
|
var import_uuid = require("@ai-matrx/kit/uuid");
|
|
495
|
+
var import_formula = require("@ai-matrx/kit/formula");
|
|
495
496
|
|
|
496
497
|
// src/templates/confidential.ts
|
|
497
498
|
var TABLE_KERNEL_ID = "11111111-0000-4000-8000-000000000001";
|
|
@@ -2087,7 +2088,7 @@ function validateUseCase(useCase) {
|
|
|
2087
2088
|
`${primary.rows.length} rows makes the screen look empty. Seed at least ${MIN_ROWS_IN_PRIMARY_TABLE} \u2014 dozens is better.`
|
|
2088
2089
|
);
|
|
2089
2090
|
}
|
|
2090
|
-
const resolveField = (tableToken,
|
|
2091
|
+
const resolveField = (tableToken, fieldKey2) => tablesByToken.get(tableToken)?.fields.find((f) => f.key === fieldKey2);
|
|
2091
2092
|
useCase.relationships?.forEach((rel, i) => {
|
|
2092
2093
|
const at = `relationships[${i}]`;
|
|
2093
2094
|
if (!tablesByToken.has(rel.fromTable)) {
|
|
@@ -2903,134 +2904,28 @@ function storeFilterMatches(row, filter, _timeZone) {
|
|
|
2903
2904
|
return have === String(want);
|
|
2904
2905
|
});
|
|
2905
2906
|
}
|
|
2906
|
-
var
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
"NOT",
|
|
2929
|
-
"OR",
|
|
2930
|
-
"REGEX_MATCH",
|
|
2931
|
-
"RIGHT",
|
|
2932
|
-
"ROUND",
|
|
2933
|
-
"SUBSTITUTE",
|
|
2934
|
-
"SUM",
|
|
2935
|
-
"SWITCH",
|
|
2936
|
-
"TODAY",
|
|
2937
|
-
"NOW",
|
|
2938
|
-
"TRIM",
|
|
2939
|
-
"UPPER",
|
|
2940
|
-
"WORKDAY",
|
|
2941
|
-
"YEAR"
|
|
2942
|
-
]);
|
|
2943
|
-
function fxTokens(text) {
|
|
2944
|
-
const out = [];
|
|
2945
|
-
let i = 0;
|
|
2946
|
-
while (i < text.length) {
|
|
2947
|
-
const c = text[i];
|
|
2948
|
-
if (/\s/.test(c)) i += 1;
|
|
2949
|
-
else if (c === "{") {
|
|
2950
|
-
const j = text.indexOf("}", i);
|
|
2951
|
-
if (j < 0) return "a { has no closing }";
|
|
2952
|
-
out.push({ t: "ref", v: text.slice(i + 1, j).trim() });
|
|
2953
|
-
i = j + 1;
|
|
2954
|
-
} else if (c === "'" || c === '"') {
|
|
2955
|
-
const j = text.indexOf(c, i + 1);
|
|
2956
|
-
if (j < 0) return "a quoted word has no closing quote";
|
|
2957
|
-
out.push({ t: "str", v: text.slice(i + 1, j) });
|
|
2958
|
-
i = j + 1;
|
|
2959
|
-
} else if (/[0-9.]/.test(c)) {
|
|
2960
|
-
const m = /^[0-9]*\.?[0-9]+/.exec(text.slice(i));
|
|
2961
|
-
out.push({ t: "num", v: m[0] });
|
|
2962
|
-
i += m[0].length;
|
|
2963
|
-
} else if (/[A-Za-z_]/.test(c)) {
|
|
2964
|
-
const m = /^[A-Za-z_][A-Za-z_0-9]*/.exec(text.slice(i));
|
|
2965
|
-
out.push({ t: "name", v: m[0] });
|
|
2966
|
-
i += m[0].length;
|
|
2967
|
-
} else if ("+-*/(),%".includes(c)) {
|
|
2968
|
-
out.push({ t: "op", v: c });
|
|
2969
|
-
i += 1;
|
|
2970
|
-
} else return `"${c}" is not part of a formula`;
|
|
2971
|
-
}
|
|
2972
|
-
return out;
|
|
2973
|
-
}
|
|
2974
|
-
function parseFormula(text) {
|
|
2975
|
-
const toks = fxTokens(text);
|
|
2976
|
-
if (typeof toks === "string") return toks;
|
|
2977
|
-
let p = 0;
|
|
2978
|
-
const peek = () => toks[p];
|
|
2979
|
-
const fail = (why) => {
|
|
2980
|
-
throw new Error(why);
|
|
2981
|
-
};
|
|
2982
|
-
const expr = () => {
|
|
2983
|
-
let a = term();
|
|
2984
|
-
while (peek()?.t === "op" && (peek().v === "+" || peek().v === "-")) {
|
|
2985
|
-
const op = toks[p++].v;
|
|
2986
|
-
a = { k: "bin", op, a, b: term() };
|
|
2987
|
-
}
|
|
2988
|
-
return a;
|
|
2989
|
-
};
|
|
2990
|
-
const term = () => {
|
|
2991
|
-
let a = factor();
|
|
2992
|
-
while (peek()?.t === "op" && (peek().v === "*" || peek().v === "/" || peek().v === "%")) {
|
|
2993
|
-
const op = toks[p++].v;
|
|
2994
|
-
a = { k: "bin", op, a, b: factor() };
|
|
2995
|
-
}
|
|
2996
|
-
return a;
|
|
2997
|
-
};
|
|
2998
|
-
const factor = () => {
|
|
2999
|
-
const tk = toks[p++];
|
|
3000
|
-
if (!tk) return fail("the formula ends too early");
|
|
3001
|
-
if (tk.t === "num") return { k: "num", v: parseNumeric(tk.v) };
|
|
3002
|
-
if (tk.t === "str") return { k: "str", v: tk.v };
|
|
3003
|
-
if (tk.t === "ref") return { k: "ref", v: tk.v };
|
|
3004
|
-
if (tk.t === "op" && tk.v === "-") return { k: "neg", a: factor() };
|
|
3005
|
-
if (tk.t === "op" && tk.v === "(") {
|
|
3006
|
-
const e = expr();
|
|
3007
|
-
if (toks[p++]?.v !== ")") fail("a ( has no closing )");
|
|
3008
|
-
return e;
|
|
3009
|
-
}
|
|
3010
|
-
if (tk.t === "name") {
|
|
3011
|
-
const f = tk.v.toUpperCase();
|
|
3012
|
-
if (!FORMULA_FUNCTIONS.has(f)) fail(`${tk.v} is not a formula function the store knows`);
|
|
3013
|
-
if (toks[p++]?.v !== "(") fail(`${f} is followed by its arguments in ( )`);
|
|
3014
|
-
const args = [];
|
|
3015
|
-
if (peek()?.v !== ")") {
|
|
3016
|
-
args.push(expr());
|
|
3017
|
-
while (peek()?.v === ",") {
|
|
3018
|
-
p += 1;
|
|
3019
|
-
args.push(expr());
|
|
3020
|
-
}
|
|
3021
|
-
}
|
|
3022
|
-
if (toks[p++]?.v !== ")") fail(`${f}( has no closing )`);
|
|
3023
|
-
return { k: "call", f, args };
|
|
3024
|
-
}
|
|
3025
|
-
return fail(`"${tk.v}" is out of place`);
|
|
3026
|
-
};
|
|
3027
|
-
try {
|
|
3028
|
-
const e = expr();
|
|
3029
|
-
if (p !== toks.length) return `"${toks[p].v}" is out of place`;
|
|
3030
|
-
return e;
|
|
3031
|
-
} catch (err) {
|
|
3032
|
-
return err.message;
|
|
3033
|
-
}
|
|
2907
|
+
var FORMULA_FUNCTION_NAMES = new Set(import_formula.FORMULA_FUNCTIONS.map((f) => f.name));
|
|
2908
|
+
function toFx(n) {
|
|
2909
|
+
switch (n.kind) {
|
|
2910
|
+
case "number":
|
|
2911
|
+
return { k: "num", v: parseNumeric(n.raw) };
|
|
2912
|
+
case "string":
|
|
2913
|
+
return { k: "str", v: n.value };
|
|
2914
|
+
case "boolean":
|
|
2915
|
+
return { k: "call", f: n.value ? "TRUE" : "FALSE", args: [] };
|
|
2916
|
+
case "reference":
|
|
2917
|
+
return { k: "ref", v: n.name };
|
|
2918
|
+
case "unary":
|
|
2919
|
+
return { k: "neg", a: toFx(n.operand) };
|
|
2920
|
+
case "binary":
|
|
2921
|
+
return { k: "bin", op: n.operator, a: toFx(n.left), b: toFx(n.right) };
|
|
2922
|
+
case "call":
|
|
2923
|
+
return { k: "call", f: n.name, args: n.args.map(toFx) };
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
function parseGateFormula(text) {
|
|
2927
|
+
const parsed = (0, import_formula.parseFormula)(text);
|
|
2928
|
+
return parsed.ok ? toFx(parsed.ast) : parsed.error;
|
|
3034
2929
|
}
|
|
3035
2930
|
function formulaRefs(n) {
|
|
3036
2931
|
if (n.k === "ref") return [n.v];
|
|
@@ -3070,7 +2965,7 @@ function evalFormula(n, valueOf) {
|
|
|
3070
2965
|
if (n.op === "*") return { ok: true, v: q(a.n * b.n, a.d * b.d) };
|
|
3071
2966
|
if (b.n === 0n) return { ok: false, why: "it divides by zero on a row" };
|
|
3072
2967
|
if (n.op === "/") return { ok: true, v: q(a.n * b.d, a.d * b.n) };
|
|
3073
|
-
return { ok: false, why:
|
|
2968
|
+
return { ok: false, why: `${n.op} is not mirrored` };
|
|
3074
2969
|
}
|
|
3075
2970
|
case "call": {
|
|
3076
2971
|
if (n.f === "ABS" && n.args.length === 1) {
|
|
@@ -3813,7 +3708,7 @@ function validateTemplate(raw, options = {}) {
|
|
|
3813
3708
|
return isComputed(pf) ? computedOf(target, pf, { key: far.key, values: far.values }, depth + 1) : { ok: true, v: far.values?.[f.pick] ?? null };
|
|
3814
3709
|
}
|
|
3815
3710
|
if (f.parityType === "formula" && f.formula) {
|
|
3816
|
-
const ast =
|
|
3711
|
+
const ast = parseGateFormula(f.formula);
|
|
3817
3712
|
if (typeof ast === "string") return { ok: false, why: ast };
|
|
3818
3713
|
let why = null;
|
|
3819
3714
|
const r2 = evalFormula(ast, (label2) => {
|
|
@@ -3843,7 +3738,7 @@ function validateTemplate(raw, options = {}) {
|
|
|
3843
3738
|
if (!f) return false;
|
|
3844
3739
|
if (isNumber(f)) return true;
|
|
3845
3740
|
if (f.parityType !== "formula" || !f.formula) return false;
|
|
3846
|
-
const ast =
|
|
3741
|
+
const ast = parseGateFormula(f.formula);
|
|
3847
3742
|
if (typeof ast === "string") return false;
|
|
3848
3743
|
const refsStored = formulaRefs(ast).every((l) => {
|
|
3849
3744
|
const rf = byToken.get(table)?.fields.find((x) => x.label === l);
|
|
@@ -4566,8 +4461,8 @@ function validateTemplate(raw, options = {}) {
|
|
|
4566
4461
|
for (const k of rule.filled ?? []) if (!filled(val(k))) say(at, `"${row.key}" is ${st}, so "${fieldOf(table, k)?.label ?? k}" is filled; it is empty.`);
|
|
4567
4462
|
for (const [k, v] of Object.entries(rule.equals ?? {})) {
|
|
4568
4463
|
const got = val(k) ?? null;
|
|
4569
|
-
const
|
|
4570
|
-
if (!
|
|
4464
|
+
const same4 = sameJson(got, v) || typeof v === "number" && parseNumeric(storeText(got) ?? "") !== null && qCmp(parseNumeric(storeText(got) ?? ""), parseNumeric(String(v))) === 0;
|
|
4465
|
+
if (!same4) say(at, `"${row.key}" is ${st}, so "${fieldOf(table, k)?.label ?? k}" is ${JSON.stringify(v)}; it is ${JSON.stringify(got)}.`);
|
|
4571
4466
|
}
|
|
4572
4467
|
}
|
|
4573
4468
|
};
|
|
@@ -5150,7 +5045,7 @@ function validateTemplate(raw, options = {}) {
|
|
|
5150
5045
|
say(fat, `"${f.label}" is a formula; write it in formula, naming fields in braces: {Total} - {Paid}.`);
|
|
5151
5046
|
continue;
|
|
5152
5047
|
}
|
|
5153
|
-
const ast =
|
|
5048
|
+
const ast = parseGateFormula(f.formula);
|
|
5154
5049
|
if (typeof ast === "string") {
|
|
5155
5050
|
say(fat, `The formula "${f.formula}" cannot be read: ${ast}.`);
|
|
5156
5051
|
continue;
|
|
@@ -5905,7 +5800,7 @@ ${e.body}`.matchAll(MERGE_TOKEN)].map((m) => m[1]));
|
|
|
5905
5800
|
}
|
|
5906
5801
|
}
|
|
5907
5802
|
const rows = t.rows;
|
|
5908
|
-
const
|
|
5803
|
+
const same4 = (x, y) => {
|
|
5909
5804
|
const a = storeText(x);
|
|
5910
5805
|
const b = storeText(y);
|
|
5911
5806
|
if (a === null || b === null) return false;
|
|
@@ -5923,7 +5818,7 @@ ${e.body}`.matchAll(MERGE_TOKEN)].map((m) => m[1]));
|
|
|
5923
5818
|
};
|
|
5924
5819
|
for (const sf of t.fields.filter((ff) => !isComputed(ff) && !isRelation(ff) && ff.key !== l.key)) {
|
|
5925
5820
|
const have = rows.filter((r) => filled(r.values?.[sf.key]));
|
|
5926
|
-
if (have.length >= 3 && have.every((r) =>
|
|
5821
|
+
if (have.length >= 3 && have.every((r) => same4(r.values?.[sf.key], far(r)))) {
|
|
5927
5822
|
say(`tables (${t.token}).fields (${sf.key})`, `"${sf.label}" repeats "${l.label}" on every row; drop it and use the lookup, which follows the linked record.`);
|
|
5928
5823
|
}
|
|
5929
5824
|
}
|
|
@@ -6951,6 +6846,370 @@ function normalizeComputedFields(spec, fixes) {
|
|
|
6951
6846
|
}
|
|
6952
6847
|
}
|
|
6953
6848
|
|
|
6849
|
+
// src/templates/describe/mechanical-shapes.ts
|
|
6850
|
+
var squash2 = (s) => String(s ?? "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
6851
|
+
var singular2 = (s) => s.replace(/ies$/, "y").replace(/s$/, "");
|
|
6852
|
+
var same3 = (a, b) => squash2(a) !== "" && (squash2(a) === squash2(b) || singular2(squash2(a)) === singular2(squash2(b)));
|
|
6853
|
+
var tableOf = (spec, token) => allTables(spec).find((t) => t.token === token);
|
|
6854
|
+
function fieldKey(t, name) {
|
|
6855
|
+
if (!t || typeof name !== "string") return void 0;
|
|
6856
|
+
return t.fields.find((f) => f.key === name)?.key ?? t.fields.find((f) => same3(f.key, name))?.key ?? t.fields.find((f) => same3(f.label, name))?.key;
|
|
6857
|
+
}
|
|
6858
|
+
var VIEW_ALIASES = {
|
|
6859
|
+
groupBy: ["group_by", "groupby", "groupField", "group_field", "columnField", "column_field"],
|
|
6860
|
+
dateField: ["date_field", "datefield", "dateColumn", "date_column", "calendarField"],
|
|
6861
|
+
startField: ["start_field", "startDate", "start_date", "start"],
|
|
6862
|
+
endField: ["end_field", "endDate", "end_date", "end"],
|
|
6863
|
+
coverField: ["cover_field", "coverImage", "cover_image", "imageField", "image_field", "photoField", "photo_field"],
|
|
6864
|
+
swimlaneBy: ["swimlane_by", "swimlaneField", "swimlane_field", "lanes"]
|
|
6865
|
+
};
|
|
6866
|
+
var EQ_OPS = /* @__PURE__ */ new Set(["eq", "is", "equals", "equal", "=", "==", "===", "equal_to", "is_equal"]);
|
|
6867
|
+
var NEQ_OPS = /* @__PURE__ */ new Set(["neq", "ne", "is_not", "isnot", "not_equal", "!=", "<>", "not_equals", "not_in", "nin", "not_any_of"]);
|
|
6868
|
+
var IN_OPS = /* @__PURE__ */ new Set(["in", "is_any_of", "any_of", "one_of", "in_list", "is_one_of"]);
|
|
6869
|
+
var EMPTY_OPS = /* @__PURE__ */ new Set(["is_empty", "empty", "is_null", "null", "unset", "is_blank", "blank", "is_unset"]);
|
|
6870
|
+
function sortDirection(v) {
|
|
6871
|
+
const s = String(v ?? "").toLowerCase().replace(/[^a-z]/g, "");
|
|
6872
|
+
if (["asc", "ascending", "up", "az", "oldest", "lowest", "first"].includes(s)) return "asc";
|
|
6873
|
+
if (["desc", "descending", "down", "za", "newest", "highest", "last"].includes(s)) return "desc";
|
|
6874
|
+
return void 0;
|
|
6875
|
+
}
|
|
6876
|
+
function fixViewShapes(spec, fixes) {
|
|
6877
|
+
for (const view of spec.views ?? []) {
|
|
6878
|
+
const t = tableOf(spec, view.table);
|
|
6879
|
+
const at = `views.${view.token}`;
|
|
6880
|
+
for (const [canon, alts] of Object.entries(VIEW_ALIASES)) {
|
|
6881
|
+
if (view[canon] == null) {
|
|
6882
|
+
const alt = alts.find((a) => view[a] != null && typeof view[a] === "string");
|
|
6883
|
+
if (alt) {
|
|
6884
|
+
view[canon] = view[alt];
|
|
6885
|
+
delete view[alt];
|
|
6886
|
+
fixes.push(`${at}.${canon}: written as "${alt}"`);
|
|
6887
|
+
}
|
|
6888
|
+
}
|
|
6889
|
+
if (typeof view[canon] === "string") {
|
|
6890
|
+
const k = fieldKey(t, view[canon]);
|
|
6891
|
+
if (k && k !== view[canon]) {
|
|
6892
|
+
fixes.push(`${at}.${canon}: "${view[canon]}" is the field "${k}"`);
|
|
6893
|
+
view[canon] = k;
|
|
6894
|
+
}
|
|
6895
|
+
}
|
|
6896
|
+
}
|
|
6897
|
+
const rawSorts = view.sorts ?? view.sort ?? view.orderBy ?? view.order_by;
|
|
6898
|
+
if (rawSorts !== void 0) {
|
|
6899
|
+
const list = Array.isArray(rawSorts) ? rawSorts : typeof rawSorts === "string" ? rawSorts.split(",") : rawSorts && typeof rawSorts === "object" ? [rawSorts] : [];
|
|
6900
|
+
const out = [];
|
|
6901
|
+
for (const s of list) {
|
|
6902
|
+
let name, dir;
|
|
6903
|
+
if (typeof s === "string") {
|
|
6904
|
+
const m = /^\s*(-?)\s*([^\s]+)(?:\s+(asc|desc|ascending|descending))?\s*$/i.exec(s);
|
|
6905
|
+
if (m) {
|
|
6906
|
+
name = m[2];
|
|
6907
|
+
dir = m[3] ?? (m[1] ? "desc" : "asc");
|
|
6908
|
+
}
|
|
6909
|
+
} else if (s && typeof s === "object") {
|
|
6910
|
+
name = s.field ?? s.key ?? s.column ?? s.by ?? s.name;
|
|
6911
|
+
dir = s.direction ?? s.dir ?? s.order ?? s.sort ?? "asc";
|
|
6912
|
+
}
|
|
6913
|
+
const key = fieldKey(t, name) ?? (typeof name === "string" ? name : void 0);
|
|
6914
|
+
const d = sortDirection(dir);
|
|
6915
|
+
if (key && d) out.push({ field: key, direction: d });
|
|
6916
|
+
}
|
|
6917
|
+
const changed = JSON.stringify(rawSorts) !== JSON.stringify(out) || view.sort !== void 0 || view.orderBy !== void 0 || view.order_by !== void 0;
|
|
6918
|
+
delete view.sort;
|
|
6919
|
+
delete view.orderBy;
|
|
6920
|
+
delete view.order_by;
|
|
6921
|
+
if (out.length) view.sorts = out;
|
|
6922
|
+
else delete view.sorts;
|
|
6923
|
+
if (changed) fixes.push(`${at}.sorts: written in the store's shape [{field, direction}]`);
|
|
6924
|
+
}
|
|
6925
|
+
if (Array.isArray(view.filters)) {
|
|
6926
|
+
const filters = {};
|
|
6927
|
+
const anyOf = { ...view.anyOf ?? {} };
|
|
6928
|
+
const dropped = [];
|
|
6929
|
+
for (const c of view.filters) {
|
|
6930
|
+
if (!c || typeof c !== "object") continue;
|
|
6931
|
+
const name = c.field ?? c.key ?? c.column ?? c.name;
|
|
6932
|
+
const key = fieldKey(t, name) ?? (typeof name === "string" ? name : void 0);
|
|
6933
|
+
if (!key) continue;
|
|
6934
|
+
const f = t?.fields.find((x) => x.key === key);
|
|
6935
|
+
const op = String(c.op ?? c.operator ?? c.comparison ?? "eq").toLowerCase().trim().replace(/[\s-]+/g, "_");
|
|
6936
|
+
const has = "value" in c || "values" in c;
|
|
6937
|
+
const val = "value" in c ? c.value : c.values;
|
|
6938
|
+
const setEq = (v) => {
|
|
6939
|
+
if (key in filters || key in anyOf) dropped.push(`${key} (two conditions on one field)`);
|
|
6940
|
+
else filters[key] = v;
|
|
6941
|
+
};
|
|
6942
|
+
const setAny = (vals) => {
|
|
6943
|
+
if (key in filters || key in anyOf) dropped.push(`${key} (two conditions on one field)`);
|
|
6944
|
+
else if (vals.length === 1) filters[key] = vals[0];
|
|
6945
|
+
else anyOf[key] = vals;
|
|
6946
|
+
};
|
|
6947
|
+
const universe = f?.parityType === "checkbox" ? [true, false] : f?.choices?.length ? [...f.choices] : void 0;
|
|
6948
|
+
if (EMPTY_OPS.has(op)) setEq(null);
|
|
6949
|
+
else if (EQ_OPS.has(op) && has && !Array.isArray(val)) setEq(val);
|
|
6950
|
+
else if ((IN_OPS.has(op) || EQ_OPS.has(op)) && Array.isArray(val) && val.length) setAny(val);
|
|
6951
|
+
else if (NEQ_OPS.has(op) && has && universe) {
|
|
6952
|
+
const gone = new Set(Array.isArray(val) ? val : [val]);
|
|
6953
|
+
const left = universe.filter((x) => !gone.has(x));
|
|
6954
|
+
if (left.length) setAny(left);
|
|
6955
|
+
else dropped.push(`${key} ${op}`);
|
|
6956
|
+
} else dropped.push(`${key} ${op}`);
|
|
6957
|
+
}
|
|
6958
|
+
view.filters = filters;
|
|
6959
|
+
if (Object.keys(anyOf).length) view.anyOf = anyOf;
|
|
6960
|
+
fixes.push(`${at}.filters: an array of conditions written as the store's map (field \u2192 value; several values \u2192 anyOf)${dropped.length ? `; not expressible and dropped, so the view shows more rows than asked: ${dropped.join(", ")}` : ""}`);
|
|
6961
|
+
}
|
|
6962
|
+
}
|
|
6963
|
+
}
|
|
6964
|
+
function dropDuplicateViews(spec, fixes) {
|
|
6965
|
+
const seen = /* @__PURE__ */ new Map();
|
|
6966
|
+
const kept = [];
|
|
6967
|
+
for (const v of spec.views ?? []) {
|
|
6968
|
+
const { token: _t, name: _n, isDefault: _d, ...shape } = v;
|
|
6969
|
+
const sig = JSON.stringify(Object.keys(shape).sort().map((k) => [k, shape[k]]));
|
|
6970
|
+
const twin = seen.get(sig);
|
|
6971
|
+
if (!twin) {
|
|
6972
|
+
seen.set(sig, v);
|
|
6973
|
+
kept.push(v);
|
|
6974
|
+
continue;
|
|
6975
|
+
}
|
|
6976
|
+
if (v.isDefault) twin.isDefault = true;
|
|
6977
|
+
fixes.push(`views.${v.token}: "${v.name}" showed exactly what "${twin.name}" shows; dropped`);
|
|
6978
|
+
}
|
|
6979
|
+
if (kept.length !== (spec.views ?? []).length) spec.views = kept;
|
|
6980
|
+
}
|
|
6981
|
+
var KIND_KEYS = ["kind", "type", "fieldType", "field_type", "dataType", "data_type", "parity_type"];
|
|
6982
|
+
var KIND_SYNONYMS = {
|
|
6983
|
+
string: "text",
|
|
6984
|
+
shorttext: "text",
|
|
6985
|
+
singlelinetext: "text",
|
|
6986
|
+
varchar: "text",
|
|
6987
|
+
longtext: "long_text",
|
|
6988
|
+
textarea: "long_text",
|
|
6989
|
+
paragraph: "long_text",
|
|
6990
|
+
multiline: "long_text",
|
|
6991
|
+
richtext: "long_text",
|
|
6992
|
+
integer: "number",
|
|
6993
|
+
int: "number",
|
|
6994
|
+
decimal: "number",
|
|
6995
|
+
float: "number",
|
|
6996
|
+
numeric: "number",
|
|
6997
|
+
money: "currency",
|
|
6998
|
+
price: "currency",
|
|
6999
|
+
amount: "currency",
|
|
7000
|
+
boolean: "checkbox",
|
|
7001
|
+
bool: "checkbox",
|
|
7002
|
+
tick: "checkbox",
|
|
7003
|
+
yesno: "checkbox",
|
|
7004
|
+
date: "datetime",
|
|
7005
|
+
timestamp: "datetime",
|
|
7006
|
+
datetime: "datetime",
|
|
7007
|
+
time: "datetime",
|
|
7008
|
+
dropdown: "select",
|
|
7009
|
+
enum: "select",
|
|
7010
|
+
choice: "select",
|
|
7011
|
+
singleselect: "select",
|
|
7012
|
+
multiselect: "multi_select",
|
|
7013
|
+
tags: "multi_select",
|
|
7014
|
+
tel: "phone",
|
|
7015
|
+
telephone: "phone",
|
|
7016
|
+
mobile: "phone",
|
|
7017
|
+
mail: "email",
|
|
7018
|
+
emailaddress: "email",
|
|
7019
|
+
website: "url",
|
|
7020
|
+
link: "url",
|
|
7021
|
+
uri: "url"
|
|
7022
|
+
};
|
|
7023
|
+
function inferKind(label, key) {
|
|
7024
|
+
const s = `${key.replace(/_/g, " ")} ${label}`.toLowerCase();
|
|
7025
|
+
if (/\be-?mail\b/.test(s)) return "email";
|
|
7026
|
+
if (/\b(phone|mobile|cell|telephone|fax)\b/.test(s)) return "phone";
|
|
7027
|
+
if (/\b(url|website|web site|link)\b/.test(s)) return "url";
|
|
7028
|
+
if (/\b(amount|fee|price|cost|total|balance|budget|salary|wage|deposit|revenue|payment)\b/.test(s)) return "currency";
|
|
7029
|
+
if (/\b(date|when|due|deadline|birthday|dob|start|end|time|at)\b/.test(s) || /_(on|at)$/.test(key)) return "datetime";
|
|
7030
|
+
if (/\b(notes?|description|details|comments?|summary|instructions)\b/.test(s)) return "long_text";
|
|
7031
|
+
if (/\b(count|quantity|qty|hours|score|age|number of|size|weight|rating)\b/.test(s)) return "number";
|
|
7032
|
+
if (/\b(name|title|owner|label|code|id|reference)\b/.test(s)) return "text";
|
|
7033
|
+
return void 0;
|
|
7034
|
+
}
|
|
7035
|
+
var wordOf = (v) => {
|
|
7036
|
+
if (typeof v !== "string") return void 0;
|
|
7037
|
+
if (RESOLVABLE_FIELD_TYPE_WORDS.includes(v)) return v;
|
|
7038
|
+
const syn = KIND_SYNONYMS[squash2(v)];
|
|
7039
|
+
return syn && RESOLVABLE_FIELD_TYPE_WORDS.includes(syn) ? syn : void 0;
|
|
7040
|
+
};
|
|
7041
|
+
function fixFieldKinds(spec, fixes) {
|
|
7042
|
+
for (const t of spec.tables) {
|
|
7043
|
+
for (const f of t.fields) {
|
|
7044
|
+
const raw = f;
|
|
7045
|
+
if (typeof raw.parityType === "string" && RESOLVABLE_FIELD_TYPE_WORDS.includes(raw.parityType)) continue;
|
|
7046
|
+
const at = `tables.${t.token}.fields.${f.key}`;
|
|
7047
|
+
const alias = KIND_KEYS.find((k) => raw[k] != null && typeof raw[k] === "string");
|
|
7048
|
+
let kind = wordOf(raw.parityType) ?? (alias ? wordOf(raw[alias]) : void 0);
|
|
7049
|
+
let how = alias ? `its "${alias}" says "${raw[alias]}"` : raw.parityType ? `"${raw.parityType}" is not a type word` : "it names no type";
|
|
7050
|
+
if (!kind && !alias && !raw.parityType) {
|
|
7051
|
+
kind = inferKind(String(f.label ?? ""), String(f.key ?? ""));
|
|
7052
|
+
how = `it names no type; "${f.label}" plainly is one`;
|
|
7053
|
+
}
|
|
7054
|
+
if (!kind) continue;
|
|
7055
|
+
for (const k of KIND_KEYS) if (k !== "parityType" && raw[k] === raw[alias ?? ""] && alias) delete raw[k];
|
|
7056
|
+
f.parityType = kind;
|
|
7057
|
+
if ((kind === "phone" || kind === "email") && !raw.format) raw.format = kind;
|
|
7058
|
+
fixes.push(`${at}: type ${kind} (${how})`);
|
|
7059
|
+
}
|
|
7060
|
+
}
|
|
7061
|
+
}
|
|
7062
|
+
function fixFormulaRefs(spec, fixes) {
|
|
7063
|
+
for (const t of spec.tables) {
|
|
7064
|
+
for (const f of t.fields) {
|
|
7065
|
+
if (f.parityType !== "formula" || typeof f.formula !== "string") continue;
|
|
7066
|
+
const others = t.fields.filter((x) => x.key !== f.key);
|
|
7067
|
+
const labelFor = (name) => others.find((x) => x.label === name)?.label ?? others.find((x) => x.key === name)?.label ?? others.find((x) => same3(x.key, name))?.label ?? others.find((x) => same3(x.label, name))?.label;
|
|
7068
|
+
const text = f.formula.replace(/^\s*=\s*/, "");
|
|
7069
|
+
let out = "";
|
|
7070
|
+
let i = 0;
|
|
7071
|
+
while (i < text.length) {
|
|
7072
|
+
const c = text[i];
|
|
7073
|
+
if (c === "{") {
|
|
7074
|
+
const j = text.indexOf("}", i);
|
|
7075
|
+
if (j < 0) {
|
|
7076
|
+
out += text.slice(i);
|
|
7077
|
+
break;
|
|
7078
|
+
}
|
|
7079
|
+
const name = text.slice(i + 1, j).trim();
|
|
7080
|
+
out += `{${labelFor(name) ?? name}}`;
|
|
7081
|
+
i = j + 1;
|
|
7082
|
+
} else if (c === "'" || c === '"') {
|
|
7083
|
+
const j = text.indexOf(c, i + 1);
|
|
7084
|
+
if (j < 0) {
|
|
7085
|
+
out += text.slice(i);
|
|
7086
|
+
break;
|
|
7087
|
+
}
|
|
7088
|
+
out += text.slice(i, j + 1);
|
|
7089
|
+
i = j + 1;
|
|
7090
|
+
} else if (/[A-Za-z_]/.test(c)) {
|
|
7091
|
+
const m = /^[A-Za-z_][A-Za-z_0-9]*/.exec(text.slice(i))[0];
|
|
7092
|
+
const after = text.slice(i + m.length).trimStart()[0];
|
|
7093
|
+
const label = after === "(" ? void 0 : labelFor(m);
|
|
7094
|
+
out += label ? `{${label}}` : m;
|
|
7095
|
+
i += m.length;
|
|
7096
|
+
} else {
|
|
7097
|
+
out += c;
|
|
7098
|
+
i += 1;
|
|
7099
|
+
}
|
|
7100
|
+
}
|
|
7101
|
+
if (out !== f.formula) {
|
|
7102
|
+
fixes.push(`tables.${t.token}.fields.${f.key}.formula: fields named by label in braces ("${f.formula}" \u2192 "${out}")`);
|
|
7103
|
+
f.formula = out;
|
|
7104
|
+
}
|
|
7105
|
+
}
|
|
7106
|
+
}
|
|
7107
|
+
}
|
|
7108
|
+
function fixInverseKeys(spec, fixes) {
|
|
7109
|
+
const rels = spec.relationships ?? [];
|
|
7110
|
+
const taken = /* @__PURE__ */ new Set();
|
|
7111
|
+
for (const t of allTables(spec)) for (const f of t.fields) taken.add(`${t.token}.${f.key}`);
|
|
7112
|
+
for (const r of rels) if (r.inverseKey) taken.add(`${r.toTable}.${r.inverseKey}`);
|
|
7113
|
+
for (const r of rels) {
|
|
7114
|
+
if (!r.inverseKey) continue;
|
|
7115
|
+
const target = tableOf(spec, r.toTable);
|
|
7116
|
+
if (!target?.fields.some((f) => f.key === r.inverseKey)) continue;
|
|
7117
|
+
let k = `${r.fromTable}_${r.fromField}_back`;
|
|
7118
|
+
for (let n = 2; taken.has(`${r.toTable}.${k}`); n++) k = `${r.fromTable}_${r.fromField}_back_${n}`;
|
|
7119
|
+
fixes.push(`relationships: ${r.fromTable}.${r.fromField} \u2192 ${r.toTable}: inverseKey "${r.inverseKey}" is already a field of ${r.toTable}; renamed "${k}"`);
|
|
7120
|
+
taken.add(`${r.toTable}.${k}`);
|
|
7121
|
+
r.inverseKey = k;
|
|
7122
|
+
}
|
|
7123
|
+
}
|
|
7124
|
+
var STATUS_WORDS = /\b(open|booked|upcoming|unpaid|outstanding|active|ordered|pending|overdue|scheduled|confirmed|paid|completed|current|remaining)\b/i;
|
|
7125
|
+
function fixRollupFilters(spec, fixes) {
|
|
7126
|
+
for (const t of spec.tables) {
|
|
7127
|
+
for (const f of t.fields) {
|
|
7128
|
+
if (f.parityType !== "rollup" || f.filter && Object.keys(f.filter).length || !f.via) continue;
|
|
7129
|
+
const hit = STATUS_WORDS.exec(`${f.label} ${f.key.replace(/_/g, " ")}`);
|
|
7130
|
+
const via = t.fields.find((x) => x.key === f.via);
|
|
7131
|
+
const far = via?.relationTarget ? tableOf(spec, via.relationTarget) : void 0;
|
|
7132
|
+
if (!hit || !far) continue;
|
|
7133
|
+
const word = hit[1].toLowerCase();
|
|
7134
|
+
const cands = [];
|
|
7135
|
+
for (const x of far.fields) {
|
|
7136
|
+
if (["select", "multi_select"].includes(x.parityType)) {
|
|
7137
|
+
for (const c of x.choices ?? []) if (c.toLowerCase() === word) cands.push({ key: x.key, value: c });
|
|
7138
|
+
}
|
|
7139
|
+
if (x.parityType === "checkbox" && new RegExp(`(^|_)${word}($|_)`).test(x.key)) cands.push({ key: x.key, value: true });
|
|
7140
|
+
if (x.parityType === "checkbox" && word === "unpaid" && /(^|_)paid($|_)/.test(x.key)) cands.push({ key: x.key, value: false });
|
|
7141
|
+
}
|
|
7142
|
+
if (cands.length !== 1) continue;
|
|
7143
|
+
f.filter = { [cands[0].key]: cands[0].value };
|
|
7144
|
+
fixes.push(`tables.${t.token}.fields.${f.key}: "${hit[1]}" in its name now picks those records: filter ${JSON.stringify(f.filter)}`);
|
|
7145
|
+
}
|
|
7146
|
+
}
|
|
7147
|
+
}
|
|
7148
|
+
function fictional(p) {
|
|
7149
|
+
const h = [...p].reduce((n, ch) => (n * 31 + ch.charCodeAt(0)) % 1e3, 7);
|
|
7150
|
+
const nn = String(h % 100).padStart(2, "0");
|
|
7151
|
+
const uk = String(h).padStart(3, "0");
|
|
7152
|
+
if (/^\s*\+/.test(p) && !/^\s*\+1\b/.test(p)) return `+44 20 7946 0${uk}`;
|
|
7153
|
+
if (/^\s*0\d/.test(p)) return `020 7946 0${uk}`;
|
|
7154
|
+
return p.replace(/(\d{3})([\s.-]?)(\d{4})\s*$/, (_m, _a, sep) => `555${sep}01${nn}`);
|
|
7155
|
+
}
|
|
7156
|
+
function fixFictionalPhones(spec, fixes) {
|
|
7157
|
+
const json = JSON.stringify(spec);
|
|
7158
|
+
const hits = /* @__PURE__ */ new Set();
|
|
7159
|
+
const isPhoneField = (f) => f.parityType === "phone" || /(^|_)(phone|mobile|cell|fax|tel)(_|$)/.test(f.key);
|
|
7160
|
+
const biz = spec.business?.phone;
|
|
7161
|
+
if (typeof biz === "string") for (const p of phonesOutsideFictionalRange(biz, true)) hits.add(p);
|
|
7162
|
+
for (const t of spec.tables) for (const r of t.rows ?? []) for (const f of t.fields) {
|
|
7163
|
+
const v = r.values?.[f.key];
|
|
7164
|
+
if (typeof v === "string") for (const p of phonesOutsideFictionalRange(v, isPhoneField(f))) hits.add(p);
|
|
7165
|
+
}
|
|
7166
|
+
for (const p of phonesOutsideFictionalRange(json)) hits.add(p);
|
|
7167
|
+
let out = json;
|
|
7168
|
+
let n = 0;
|
|
7169
|
+
for (const p of hits) {
|
|
7170
|
+
const escaped = JSON.stringify(p).slice(1, -1);
|
|
7171
|
+
if (!out.includes(escaped)) continue;
|
|
7172
|
+
out = out.split(escaped).join(JSON.stringify(fictional(p)).slice(1, -1));
|
|
7173
|
+
n += 1;
|
|
7174
|
+
}
|
|
7175
|
+
if (!n) return;
|
|
7176
|
+
const next = JSON.parse(out);
|
|
7177
|
+
for (const k of Object.keys(spec)) delete spec[k];
|
|
7178
|
+
Object.assign(spec, next);
|
|
7179
|
+
fixes.push(`phones: ${n} number(s) that could reach a real phone replaced with fictional ones (555-0100\u20260199, UK Ofcom drama range)`);
|
|
7180
|
+
}
|
|
7181
|
+
function fixTeamBlockCopy(spec, fixes) {
|
|
7182
|
+
const block = SHARED_BLOCKS.team_member;
|
|
7183
|
+
const used = /* @__PURE__ */ new Set();
|
|
7184
|
+
for (const v of spec.views ?? []) for (const k of ["groupBy", "dateField", "startField", "endField", "coverField", "swimlaneBy", "measure"]) if (v.table && v[k]) used.add(`${v.table}.${v[k]}`);
|
|
7185
|
+
for (const fm of spec.forms ?? []) for (const q2 of fm.fields ?? []) used.add(`${fm.table}.${q2.fieldKey}`);
|
|
7186
|
+
for (const e of spec.extras ?? []) for (const q2 of e.questions ?? []) used.add(`${e.table}.${q2.fieldKey}`);
|
|
7187
|
+
for (const t of spec.tables) {
|
|
7188
|
+
const meaning = (f) => block.fields.find((bf) => bf.parityType === f.parityType && bf.sameAs.some((w) => same3(w, f.label) || same3(w, f.key)));
|
|
7189
|
+
const hits = t.fields.map((f) => ({ f, bf: meaning(f) })).filter((x) => x.bf);
|
|
7190
|
+
if (hits.length < block.copyThreshold) continue;
|
|
7191
|
+
const hired = hits.some((h) => h.bf.key === "start_date");
|
|
7192
|
+
const hasRole = hits.some((h) => h.bf.key === "role");
|
|
7193
|
+
const contactOfSomeone = t.fields.some((f) => f.parityType === "relation" && f.relationTarget && f.relationTarget !== block.table && spec.tables.some((o) => o.token === f.relationTarget));
|
|
7194
|
+
if (!(hired || hasRole && !contactOfSomeone)) continue;
|
|
7195
|
+
const order = ["full_name", "role", "work_email", "mobile", "start_date"];
|
|
7196
|
+
const keep = new Set(hits.filter((h) => ["full_name", "role"].includes(h.bf.key)).map((h) => h.f.key));
|
|
7197
|
+
const dropped = [];
|
|
7198
|
+
let left = hits.length;
|
|
7199
|
+
for (const h of [...hits].sort((a, b) => order.indexOf(b.bf.key) - order.indexOf(a.bf.key))) {
|
|
7200
|
+
if (left < block.copyThreshold) break;
|
|
7201
|
+
if (keep.has(h.f.key) || used.has(`${t.token}.${h.f.key}`) || t.titleField === h.f.key) continue;
|
|
7202
|
+
dropped.push(h.f.label);
|
|
7203
|
+
t.fields = t.fields.filter((x) => x.key !== h.f.key);
|
|
7204
|
+
for (const r of t.rows ?? []) if (r.values) delete r.values[h.f.key];
|
|
7205
|
+
left -= 1;
|
|
7206
|
+
}
|
|
7207
|
+
if (left >= block.copyThreshold) continue;
|
|
7208
|
+
if (!t.fields.some((f) => f.key === "notes")) t.fields.push({ key: "notes", label: "Notes", parityType: "long_text", sensitivity: "internal", contextPolicy: "include" });
|
|
7209
|
+
fixes.push(`tables.${t.token}: copied the shared team_member block; kept as an ordinary table (${t.fields.map((f) => f.label).join(", ")}); dropped ${dropped.join(", ") || "nothing"}`);
|
|
7210
|
+
}
|
|
7211
|
+
}
|
|
7212
|
+
|
|
6954
7213
|
// src/templates/describe/autofix.ts
|
|
6955
7214
|
var clone2 = (v) => JSON.parse(JSON.stringify(v));
|
|
6956
7215
|
var SENSITIVITY_RANK2 = ["public", "internal", "confidential", "restricted"];
|
|
@@ -7272,7 +7531,15 @@ function switchJob(spec, fixes) {
|
|
|
7272
7531
|
function autofixDescribeSpec(input, ctx = {}) {
|
|
7273
7532
|
const spec = describeSpec(clone2(input));
|
|
7274
7533
|
const autoFixes = [];
|
|
7534
|
+
fixFieldKinds(spec, autoFixes);
|
|
7275
7535
|
normalizeComputedFields(spec, autoFixes);
|
|
7536
|
+
fixFormulaRefs(spec, autoFixes);
|
|
7537
|
+
fixViewShapes(spec, autoFixes);
|
|
7538
|
+
dropDuplicateViews(spec, autoFixes);
|
|
7539
|
+
fixInverseKeys(spec, autoFixes);
|
|
7540
|
+
fixRollupFilters(spec, autoFixes);
|
|
7541
|
+
fixTeamBlockCopy(spec, autoFixes);
|
|
7542
|
+
fixFictionalPhones(spec, autoFixes);
|
|
7276
7543
|
fixMissingQuestionFields(spec, autoFixes);
|
|
7277
7544
|
raiseSensitivity(spec, autoFixes);
|
|
7278
7545
|
flagAskedOfSubject(spec, ctx, autoFixes);
|
|
@@ -7504,6 +7771,22 @@ var LOOKUP_SHAPE_FACTS = {
|
|
|
7504
7771
|
rule: 'a lookup is a field that shows one value of the record a relation field points at: parityType "lookup", via = the key of a relation field of THIS table, pick = the key of a field on the far table. Write exactly these keys on the field itself \u2014 never a nested {lookup:{\u2026}} object.',
|
|
7505
7772
|
example: { key: "client_phone", label: "Client phone", parityType: "lookup", via: "client", pick: "phone" }
|
|
7506
7773
|
};
|
|
7774
|
+
var VIEW_FILTERS_SHAPE_FACTS = {
|
|
7775
|
+
rule: "a view's `filters` is ONE object of field key \u2192 value (a plain value is equality, null means empty); several allowed values of one field go in `anyOf` as field key \u2192 [values]; `sorts` is a list of {field, direction: asc|desc}; groupBy, dateField, startField, endField, coverField are field KEYS. Never write filters or sort as a list of {field, op, value}. The store cannot filter on 'not', 'contains', 'less than' or 'is not empty' \u2014 leave such a condition out.",
|
|
7776
|
+
example: { token: "open_requests", name: "Open requests", table: "request", kind: "kanban", groupBy: "stage", filters: { priority: "High" }, anyOf: { stage: ["New", "In progress"] }, sorts: [{ field: "due_on", direction: "asc" }] }
|
|
7777
|
+
};
|
|
7778
|
+
var FIELD_SHAPE_FACTS = {
|
|
7779
|
+
rule: "every field is {key, label, parityType, sensitivity, contextPolicy}; the type word goes in `parityType` (never `kind` or `type`) and is one of field_kinds.",
|
|
7780
|
+
example: { key: "visit_fee", label: "Visit fee", parityType: "currency", sensitivity: "internal", contextPolicy: "include" }
|
|
7781
|
+
};
|
|
7782
|
+
var FORMULA_SHAPE_FACTS = {
|
|
7783
|
+
rule: "a formula names every field by its LABEL in braces ({Quantity}), never by key; operators are + - * / % only (no >, <, = or comparisons), functions are IF, ROUND, SUM, DATEDIFF and the other store functions; it never reads a lookup or rollup, nor itself.",
|
|
7784
|
+
example: { key: "line_total", label: "Line total", parityType: "formula", formula: "{Quantity} * {Unit price}" }
|
|
7785
|
+
};
|
|
7786
|
+
var RELATIONSHIP_SHAPE_FACTS = {
|
|
7787
|
+
rule: "a relationship's inverseKey is the name the OTHER table sees this link under; it must not be a key that table already has as a field. A parent that lists its children owns ONE multi relation field (multi true); the child's single link to the parent is a second field, and their inverseKeys differ from every field key.",
|
|
7788
|
+
example: { fromTable: "request", fromField: "client", toTable: "client", flavor: "referenced", cardinality: "one", onDelete: "restrict", inverseKey: "requests_from" }
|
|
7789
|
+
};
|
|
7507
7790
|
function describeVocabulary() {
|
|
7508
7791
|
return {
|
|
7509
7792
|
field_kinds: RESOLVABLE_FIELD_TYPE_WORDS,
|
|
@@ -7529,6 +7812,12 @@ function describeVocabulary() {
|
|
|
7529
7812
|
view_shape: VIEW_SHAPE_FACTS,
|
|
7530
7813
|
rollup_shape: ROLLUP_SHAPE_FACTS,
|
|
7531
7814
|
lookup_shape: LOOKUP_SHAPE_FACTS,
|
|
7815
|
+
view_filters_shape: VIEW_FILTERS_SHAPE_FACTS,
|
|
7816
|
+
field_shape: FIELD_SHAPE_FACTS,
|
|
7817
|
+
formula_shape: FORMULA_SHAPE_FACTS,
|
|
7818
|
+
relationship_shape: RELATIONSHIP_SHAPE_FACTS,
|
|
7819
|
+
phone_rule: "seed phone numbers are fictional: 555-0100 to 555-0199 in North America (e.g. (813) 555-0142) or a UK Ofcom drama number (e.g. 020 7946 0123); never a real-looking number.",
|
|
7820
|
+
team_rule: "the team (name, role, work email, mobile, start date) is the shared team_member block; do not declare a table with those fields \u2014 keep a table of your own to just name and role, or point a relation field at team_member.",
|
|
7532
7821
|
title_field: "a table's titleField is the key of a plain text-like field of that same table (a name, a title) \u2014 never a link to another table, a formula or a timed date"
|
|
7533
7822
|
};
|
|
7534
7823
|
}
|