@ai-matrx/records 0.81.3 → 0.82.1
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 +23 -0
- package/dist/app-table/index.cjs +1 -1
- package/dist/app-table/index.cjs.map +1 -1
- package/dist/app-table/index.js +1 -1
- package/dist/app-table/index.js.map +1 -1
- package/dist/core/index.cjs +3 -3
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.js +3 -3
- package/dist/core/index.js.map +1 -1
- package/dist/data/index.cjs +1 -1
- package/dist/data/index.cjs.map +1 -1
- package/dist/data/index.js +1 -1
- package/dist/data/index.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +1 -1
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.js +1 -1
- package/dist/react/index.js.map +1 -1
- package/dist/templates/index.cjs +542 -5
- package/dist/templates/index.cjs.map +1 -1
- package/dist/templates/index.js +542 -5
- package/dist/templates/index.js.map +1 -1
- package/package.json +4 -4
package/dist/templates/index.js
CHANGED
|
@@ -1922,7 +1922,7 @@ function validateUseCase(useCase) {
|
|
|
1922
1922
|
`${primary.rows.length} rows makes the screen look empty. Seed at least ${MIN_ROWS_IN_PRIMARY_TABLE} \u2014 dozens is better.`
|
|
1923
1923
|
);
|
|
1924
1924
|
}
|
|
1925
|
-
const resolveField = (tableToken,
|
|
1925
|
+
const resolveField = (tableToken, fieldKey2) => tablesByToken.get(tableToken)?.fields.find((f) => f.key === fieldKey2);
|
|
1926
1926
|
useCase.relationships?.forEach((rel, i) => {
|
|
1927
1927
|
const at = `relationships[${i}]`;
|
|
1928
1928
|
if (!tablesByToken.has(rel.fromTable)) {
|
|
@@ -4401,8 +4401,8 @@ function validateTemplate(raw, options = {}) {
|
|
|
4401
4401
|
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.`);
|
|
4402
4402
|
for (const [k, v] of Object.entries(rule.equals ?? {})) {
|
|
4403
4403
|
const got = val(k) ?? null;
|
|
4404
|
-
const
|
|
4405
|
-
if (!
|
|
4404
|
+
const same4 = sameJson(got, v) || typeof v === "number" && parseNumeric(storeText(got) ?? "") !== null && qCmp(parseNumeric(storeText(got) ?? ""), parseNumeric(String(v))) === 0;
|
|
4405
|
+
if (!same4) say(at, `"${row.key}" is ${st}, so "${fieldOf(table, k)?.label ?? k}" is ${JSON.stringify(v)}; it is ${JSON.stringify(got)}.`);
|
|
4406
4406
|
}
|
|
4407
4407
|
}
|
|
4408
4408
|
};
|
|
@@ -5740,7 +5740,7 @@ ${e.body}`.matchAll(MERGE_TOKEN)].map((m) => m[1]));
|
|
|
5740
5740
|
}
|
|
5741
5741
|
}
|
|
5742
5742
|
const rows = t.rows;
|
|
5743
|
-
const
|
|
5743
|
+
const same4 = (x, y) => {
|
|
5744
5744
|
const a = storeText(x);
|
|
5745
5745
|
const b = storeText(y);
|
|
5746
5746
|
if (a === null || b === null) return false;
|
|
@@ -5758,7 +5758,7 @@ ${e.body}`.matchAll(MERGE_TOKEN)].map((m) => m[1]));
|
|
|
5758
5758
|
};
|
|
5759
5759
|
for (const sf of t.fields.filter((ff) => !isComputed(ff) && !isRelation(ff) && ff.key !== l.key)) {
|
|
5760
5760
|
const have = rows.filter((r) => filled(r.values?.[sf.key]));
|
|
5761
|
-
if (have.length >= 3 && have.every((r) =>
|
|
5761
|
+
if (have.length >= 3 && have.every((r) => same4(r.values?.[sf.key], far(r)))) {
|
|
5762
5762
|
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.`);
|
|
5763
5763
|
}
|
|
5764
5764
|
}
|
|
@@ -6654,6 +6654,502 @@ function mergeDescribeParts(plan, tables, doors, views, existingTables = [], not
|
|
|
6654
6654
|
return spec;
|
|
6655
6655
|
}
|
|
6656
6656
|
|
|
6657
|
+
// src/templates/describe/computed-shape.ts
|
|
6658
|
+
var VIA_KEYS = ["via", "relation", "relationField", "relation_field", "relationKey", "through", "rollupVia", "lookupVia", "rollupRelation", "lookupRelation", "rollupThrough", "lookupThrough", "from", "link", "linkField"];
|
|
6659
|
+
var PICK_KEYS = ["pick", "field", "targetField", "target_field", "fieldKey", "rollupField", "lookupField", "rollupTarget", "lookupTarget", "target", "of", "column", "sourceField", "source"];
|
|
6660
|
+
var AGG_KEYS = ["agg", "fn", "func", "function", "operation", "aggregation", "aggregate", "op", "aggregator", "calculation", "rollupFn", "rollupFunction", "rollupAgg", "rollupAggregate", "rollupAggregation", "rollupOperation", "rollupOp"];
|
|
6661
|
+
var AGG_WORDS = {
|
|
6662
|
+
sum: "sum",
|
|
6663
|
+
total: "sum",
|
|
6664
|
+
add: "sum",
|
|
6665
|
+
sumof: "sum",
|
|
6666
|
+
summation: "sum",
|
|
6667
|
+
count: "count",
|
|
6668
|
+
countall: "count",
|
|
6669
|
+
countrows: "count",
|
|
6670
|
+
number: "count",
|
|
6671
|
+
numberof: "count",
|
|
6672
|
+
n: "count",
|
|
6673
|
+
tally: "count",
|
|
6674
|
+
avg: "avg",
|
|
6675
|
+
average: "avg",
|
|
6676
|
+
mean: "avg",
|
|
6677
|
+
min: "min",
|
|
6678
|
+
minimum: "min",
|
|
6679
|
+
lowest: "min",
|
|
6680
|
+
smallest: "min",
|
|
6681
|
+
earliest: "min",
|
|
6682
|
+
max: "max",
|
|
6683
|
+
maximum: "max",
|
|
6684
|
+
highest: "max",
|
|
6685
|
+
largest: "max",
|
|
6686
|
+
latest: "max",
|
|
6687
|
+
median: "median",
|
|
6688
|
+
filled: "filled",
|
|
6689
|
+
countfilled: "filled",
|
|
6690
|
+
empty: "empty",
|
|
6691
|
+
countempty: "empty",
|
|
6692
|
+
unique: "unique",
|
|
6693
|
+
distinct: "unique",
|
|
6694
|
+
countunique: "unique",
|
|
6695
|
+
countdistinct: "unique"
|
|
6696
|
+
};
|
|
6697
|
+
var squash = (s) => String(s ?? "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
6698
|
+
var singular = (s) => s.replace(/ies$/, "y").replace(/s$/, "");
|
|
6699
|
+
var same2 = (a, b) => squash(a) === squash(b) || singular(squash(a)) === singular(squash(b));
|
|
6700
|
+
function aggWord(v) {
|
|
6701
|
+
return typeof v === "string" ? AGG_WORDS[squash(v)] : void 0;
|
|
6702
|
+
}
|
|
6703
|
+
function take(f, nested, keys) {
|
|
6704
|
+
for (const k of keys) if (nested && nested[k] !== void 0 && nested[k] !== null && nested[k] !== "") return { value: nested[k], key: k, from: "nested" };
|
|
6705
|
+
for (const k of keys) if (f[k] !== void 0 && f[k] !== null && f[k] !== "") return { value: f[k], key: k, from: "field" };
|
|
6706
|
+
return void 0;
|
|
6707
|
+
}
|
|
6708
|
+
function normalizeComputedFields(spec, fixes) {
|
|
6709
|
+
const every = allTables(spec);
|
|
6710
|
+
const byToken = new Map(every.map((t) => [t.token, t]));
|
|
6711
|
+
for (const t of spec.tables) {
|
|
6712
|
+
for (const f of t.fields) {
|
|
6713
|
+
const raw = f;
|
|
6714
|
+
const nestedKind = ["rollup", "lookup"].find((k) => raw[k] && typeof raw[k] === "object");
|
|
6715
|
+
const kind = f.parityType === "rollup" || f.parityType === "lookup" ? f.parityType : !f.parityType && nestedKind ? nestedKind : void 0;
|
|
6716
|
+
if (!kind) continue;
|
|
6717
|
+
const nested = nestedKind ? raw[nestedKind] : void 0;
|
|
6718
|
+
const at = `tables.${t.token}.fields.${f.key}`;
|
|
6719
|
+
const consumed = [];
|
|
6720
|
+
let changed = false;
|
|
6721
|
+
const hasVia = typeof f.via === "string" && t.fields.some((x) => x.key === f.via);
|
|
6722
|
+
let via = hasVia ? t.fields.find((x) => x.key === f.via) : void 0;
|
|
6723
|
+
if (!via) {
|
|
6724
|
+
const got = take(raw, nested, VIA_KEYS);
|
|
6725
|
+
if (got && typeof got.value === "string") {
|
|
6726
|
+
const rels = t.fields.filter((x) => x.parityType === "relation" && x.relationTarget && x.key !== f.key);
|
|
6727
|
+
const hits = [
|
|
6728
|
+
rels.filter((x) => x.key === got.value),
|
|
6729
|
+
rels.filter((x) => same2(x.key, got.value)),
|
|
6730
|
+
rels.filter((x) => same2(x.label, got.value)),
|
|
6731
|
+
rels.filter((x) => same2(x.relationTarget, got.value))
|
|
6732
|
+
].find((h) => h.length === 1);
|
|
6733
|
+
if (hits) {
|
|
6734
|
+
via = hits[0];
|
|
6735
|
+
f.via = via.key;
|
|
6736
|
+
consumed.push([got.from, got.key]);
|
|
6737
|
+
changed = true;
|
|
6738
|
+
}
|
|
6739
|
+
}
|
|
6740
|
+
}
|
|
6741
|
+
if (!via || !via.relationTarget) {
|
|
6742
|
+
if (changed) fixes.push(`${at}: ${kind} written as ${nestedKind ? `{${nestedKind}:\u2026}` : "loose keys"}, read into via/pick${kind === "rollup" ? "/agg" : ""} where unambiguous`);
|
|
6743
|
+
continue;
|
|
6744
|
+
}
|
|
6745
|
+
const far = byToken.get(via.relationTarget);
|
|
6746
|
+
if (kind === "rollup" && !f.agg) {
|
|
6747
|
+
const got = take(raw, nested, AGG_KEYS);
|
|
6748
|
+
const word = got ? aggWord(got.value) : void 0;
|
|
6749
|
+
if (got && word) {
|
|
6750
|
+
f.agg = word;
|
|
6751
|
+
consumed.push([got.from, got.key]);
|
|
6752
|
+
changed = true;
|
|
6753
|
+
}
|
|
6754
|
+
}
|
|
6755
|
+
if (!f.pick || !far?.fields.some((x) => x.key === f.pick)) {
|
|
6756
|
+
const got = take(raw, nested, PICK_KEYS);
|
|
6757
|
+
const named = got && typeof got.value === "string" ? got.value : void 0;
|
|
6758
|
+
const farField = named ? far?.fields.find((x) => x.key === named) ?? far?.fields.find((x) => same2(x.key, named)) ?? far?.fields.find((x) => same2(x.label, named)) : void 0;
|
|
6759
|
+
if (farField) {
|
|
6760
|
+
f.pick = farField.key;
|
|
6761
|
+
if (got) consumed.push([got.from, got.key]);
|
|
6762
|
+
changed = true;
|
|
6763
|
+
} else if (kind === "rollup" && (f.agg === "count" || !f.agg && !named) && far) {
|
|
6764
|
+
const title = far.titleField;
|
|
6765
|
+
const stand = far.fields.find((x) => x.key === title) ?? far.fields.find((x) => !["relation", "formula", "lookup", "rollup", "autonumber"].includes(x.parityType));
|
|
6766
|
+
if (stand) {
|
|
6767
|
+
f.pick = stand.key;
|
|
6768
|
+
if (got) consumed.push([got.from, got.key]);
|
|
6769
|
+
changed = true;
|
|
6770
|
+
}
|
|
6771
|
+
}
|
|
6772
|
+
}
|
|
6773
|
+
if (kind === "rollup" && changed && via && !via.multi && (via.relationMax ?? 1) <= 1) {
|
|
6774
|
+
via.multi = true;
|
|
6775
|
+
delete via.relationMax;
|
|
6776
|
+
for (const r of spec.relationships ?? []) if (r.fromTable === t.token && r.fromField === via.key) r.cardinality = "many";
|
|
6777
|
+
fixes.push(`${at}.via: "${via.key}" made a list (a rollup adds up many linked records)`);
|
|
6778
|
+
}
|
|
6779
|
+
if (!changed) continue;
|
|
6780
|
+
f.parityType = kind;
|
|
6781
|
+
if (kind === "lookup") delete raw["agg"];
|
|
6782
|
+
for (const [from, k] of consumed) if (from === "field") delete raw[k];
|
|
6783
|
+
if (nestedKind) delete raw[nestedKind];
|
|
6784
|
+
fixes.push(`${at}: ${kind} written in a shape the check does not read, rewritten as via "${f.via}", pick "${f.pick ?? "(none)"}"${kind === "rollup" ? `, agg "${f.agg ?? "(none)"}"` : ""}`);
|
|
6785
|
+
}
|
|
6786
|
+
}
|
|
6787
|
+
}
|
|
6788
|
+
|
|
6789
|
+
// src/templates/describe/mechanical-shapes.ts
|
|
6790
|
+
var squash2 = (s) => String(s ?? "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
6791
|
+
var singular2 = (s) => s.replace(/ies$/, "y").replace(/s$/, "");
|
|
6792
|
+
var same3 = (a, b) => squash2(a) !== "" && (squash2(a) === squash2(b) || singular2(squash2(a)) === singular2(squash2(b)));
|
|
6793
|
+
var tableOf = (spec, token) => allTables(spec).find((t) => t.token === token);
|
|
6794
|
+
function fieldKey(t, name) {
|
|
6795
|
+
if (!t || typeof name !== "string") return void 0;
|
|
6796
|
+
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;
|
|
6797
|
+
}
|
|
6798
|
+
var VIEW_ALIASES = {
|
|
6799
|
+
groupBy: ["group_by", "groupby", "groupField", "group_field", "columnField", "column_field"],
|
|
6800
|
+
dateField: ["date_field", "datefield", "dateColumn", "date_column", "calendarField"],
|
|
6801
|
+
startField: ["start_field", "startDate", "start_date", "start"],
|
|
6802
|
+
endField: ["end_field", "endDate", "end_date", "end"],
|
|
6803
|
+
coverField: ["cover_field", "coverImage", "cover_image", "imageField", "image_field", "photoField", "photo_field"],
|
|
6804
|
+
swimlaneBy: ["swimlane_by", "swimlaneField", "swimlane_field", "lanes"]
|
|
6805
|
+
};
|
|
6806
|
+
var EQ_OPS = /* @__PURE__ */ new Set(["eq", "is", "equals", "equal", "=", "==", "===", "equal_to", "is_equal"]);
|
|
6807
|
+
var NEQ_OPS = /* @__PURE__ */ new Set(["neq", "ne", "is_not", "isnot", "not_equal", "!=", "<>", "not_equals", "not_in", "nin", "not_any_of"]);
|
|
6808
|
+
var IN_OPS = /* @__PURE__ */ new Set(["in", "is_any_of", "any_of", "one_of", "in_list", "is_one_of"]);
|
|
6809
|
+
var EMPTY_OPS = /* @__PURE__ */ new Set(["is_empty", "empty", "is_null", "null", "unset", "is_blank", "blank", "is_unset"]);
|
|
6810
|
+
function sortDirection(v) {
|
|
6811
|
+
const s = String(v ?? "").toLowerCase().replace(/[^a-z]/g, "");
|
|
6812
|
+
if (["asc", "ascending", "up", "az", "oldest", "lowest", "first"].includes(s)) return "asc";
|
|
6813
|
+
if (["desc", "descending", "down", "za", "newest", "highest", "last"].includes(s)) return "desc";
|
|
6814
|
+
return void 0;
|
|
6815
|
+
}
|
|
6816
|
+
function fixViewShapes(spec, fixes) {
|
|
6817
|
+
for (const view of spec.views ?? []) {
|
|
6818
|
+
const t = tableOf(spec, view.table);
|
|
6819
|
+
const at = `views.${view.token}`;
|
|
6820
|
+
for (const [canon, alts] of Object.entries(VIEW_ALIASES)) {
|
|
6821
|
+
if (view[canon] == null) {
|
|
6822
|
+
const alt = alts.find((a) => view[a] != null && typeof view[a] === "string");
|
|
6823
|
+
if (alt) {
|
|
6824
|
+
view[canon] = view[alt];
|
|
6825
|
+
delete view[alt];
|
|
6826
|
+
fixes.push(`${at}.${canon}: written as "${alt}"`);
|
|
6827
|
+
}
|
|
6828
|
+
}
|
|
6829
|
+
if (typeof view[canon] === "string") {
|
|
6830
|
+
const k = fieldKey(t, view[canon]);
|
|
6831
|
+
if (k && k !== view[canon]) {
|
|
6832
|
+
fixes.push(`${at}.${canon}: "${view[canon]}" is the field "${k}"`);
|
|
6833
|
+
view[canon] = k;
|
|
6834
|
+
}
|
|
6835
|
+
}
|
|
6836
|
+
}
|
|
6837
|
+
const rawSorts = view.sorts ?? view.sort ?? view.orderBy ?? view.order_by;
|
|
6838
|
+
if (rawSorts !== void 0) {
|
|
6839
|
+
const list = Array.isArray(rawSorts) ? rawSorts : typeof rawSorts === "string" ? rawSorts.split(",") : rawSorts && typeof rawSorts === "object" ? [rawSorts] : [];
|
|
6840
|
+
const out = [];
|
|
6841
|
+
for (const s of list) {
|
|
6842
|
+
let name, dir;
|
|
6843
|
+
if (typeof s === "string") {
|
|
6844
|
+
const m = /^\s*(-?)\s*([^\s]+)(?:\s+(asc|desc|ascending|descending))?\s*$/i.exec(s);
|
|
6845
|
+
if (m) {
|
|
6846
|
+
name = m[2];
|
|
6847
|
+
dir = m[3] ?? (m[1] ? "desc" : "asc");
|
|
6848
|
+
}
|
|
6849
|
+
} else if (s && typeof s === "object") {
|
|
6850
|
+
name = s.field ?? s.key ?? s.column ?? s.by ?? s.name;
|
|
6851
|
+
dir = s.direction ?? s.dir ?? s.order ?? s.sort ?? "asc";
|
|
6852
|
+
}
|
|
6853
|
+
const key = fieldKey(t, name) ?? (typeof name === "string" ? name : void 0);
|
|
6854
|
+
const d = sortDirection(dir);
|
|
6855
|
+
if (key && d) out.push({ field: key, direction: d });
|
|
6856
|
+
}
|
|
6857
|
+
const changed = JSON.stringify(rawSorts) !== JSON.stringify(out) || view.sort !== void 0 || view.orderBy !== void 0 || view.order_by !== void 0;
|
|
6858
|
+
delete view.sort;
|
|
6859
|
+
delete view.orderBy;
|
|
6860
|
+
delete view.order_by;
|
|
6861
|
+
if (out.length) view.sorts = out;
|
|
6862
|
+
else delete view.sorts;
|
|
6863
|
+
if (changed) fixes.push(`${at}.sorts: written in the store's shape [{field, direction}]`);
|
|
6864
|
+
}
|
|
6865
|
+
if (Array.isArray(view.filters)) {
|
|
6866
|
+
const filters = {};
|
|
6867
|
+
const anyOf = { ...view.anyOf ?? {} };
|
|
6868
|
+
const dropped = [];
|
|
6869
|
+
for (const c of view.filters) {
|
|
6870
|
+
if (!c || typeof c !== "object") continue;
|
|
6871
|
+
const name = c.field ?? c.key ?? c.column ?? c.name;
|
|
6872
|
+
const key = fieldKey(t, name) ?? (typeof name === "string" ? name : void 0);
|
|
6873
|
+
if (!key) continue;
|
|
6874
|
+
const f = t?.fields.find((x) => x.key === key);
|
|
6875
|
+
const op = String(c.op ?? c.operator ?? c.comparison ?? "eq").toLowerCase().trim().replace(/[\s-]+/g, "_");
|
|
6876
|
+
const has = "value" in c || "values" in c;
|
|
6877
|
+
const val = "value" in c ? c.value : c.values;
|
|
6878
|
+
const setEq = (v) => {
|
|
6879
|
+
if (key in filters || key in anyOf) dropped.push(`${key} (two conditions on one field)`);
|
|
6880
|
+
else filters[key] = v;
|
|
6881
|
+
};
|
|
6882
|
+
const setAny = (vals) => {
|
|
6883
|
+
if (key in filters || key in anyOf) dropped.push(`${key} (two conditions on one field)`);
|
|
6884
|
+
else if (vals.length === 1) filters[key] = vals[0];
|
|
6885
|
+
else anyOf[key] = vals;
|
|
6886
|
+
};
|
|
6887
|
+
const universe = f?.parityType === "checkbox" ? [true, false] : f?.choices?.length ? [...f.choices] : void 0;
|
|
6888
|
+
if (EMPTY_OPS.has(op)) setEq(null);
|
|
6889
|
+
else if (EQ_OPS.has(op) && has && !Array.isArray(val)) setEq(val);
|
|
6890
|
+
else if ((IN_OPS.has(op) || EQ_OPS.has(op)) && Array.isArray(val) && val.length) setAny(val);
|
|
6891
|
+
else if (NEQ_OPS.has(op) && has && universe) {
|
|
6892
|
+
const gone = new Set(Array.isArray(val) ? val : [val]);
|
|
6893
|
+
const left = universe.filter((x) => !gone.has(x));
|
|
6894
|
+
if (left.length) setAny(left);
|
|
6895
|
+
else dropped.push(`${key} ${op}`);
|
|
6896
|
+
} else dropped.push(`${key} ${op}`);
|
|
6897
|
+
}
|
|
6898
|
+
view.filters = filters;
|
|
6899
|
+
if (Object.keys(anyOf).length) view.anyOf = anyOf;
|
|
6900
|
+
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(", ")}` : ""}`);
|
|
6901
|
+
}
|
|
6902
|
+
}
|
|
6903
|
+
}
|
|
6904
|
+
function dropDuplicateViews(spec, fixes) {
|
|
6905
|
+
const seen = /* @__PURE__ */ new Map();
|
|
6906
|
+
const kept = [];
|
|
6907
|
+
for (const v of spec.views ?? []) {
|
|
6908
|
+
const { token: _t, name: _n, isDefault: _d, ...shape } = v;
|
|
6909
|
+
const sig = JSON.stringify(Object.keys(shape).sort().map((k) => [k, shape[k]]));
|
|
6910
|
+
const twin = seen.get(sig);
|
|
6911
|
+
if (!twin) {
|
|
6912
|
+
seen.set(sig, v);
|
|
6913
|
+
kept.push(v);
|
|
6914
|
+
continue;
|
|
6915
|
+
}
|
|
6916
|
+
if (v.isDefault) twin.isDefault = true;
|
|
6917
|
+
fixes.push(`views.${v.token}: "${v.name}" showed exactly what "${twin.name}" shows; dropped`);
|
|
6918
|
+
}
|
|
6919
|
+
if (kept.length !== (spec.views ?? []).length) spec.views = kept;
|
|
6920
|
+
}
|
|
6921
|
+
var KIND_KEYS = ["kind", "type", "fieldType", "field_type", "dataType", "data_type", "parity_type"];
|
|
6922
|
+
var KIND_SYNONYMS = {
|
|
6923
|
+
string: "text",
|
|
6924
|
+
shorttext: "text",
|
|
6925
|
+
singlelinetext: "text",
|
|
6926
|
+
varchar: "text",
|
|
6927
|
+
longtext: "long_text",
|
|
6928
|
+
textarea: "long_text",
|
|
6929
|
+
paragraph: "long_text",
|
|
6930
|
+
multiline: "long_text",
|
|
6931
|
+
richtext: "long_text",
|
|
6932
|
+
integer: "number",
|
|
6933
|
+
int: "number",
|
|
6934
|
+
decimal: "number",
|
|
6935
|
+
float: "number",
|
|
6936
|
+
numeric: "number",
|
|
6937
|
+
money: "currency",
|
|
6938
|
+
price: "currency",
|
|
6939
|
+
amount: "currency",
|
|
6940
|
+
boolean: "checkbox",
|
|
6941
|
+
bool: "checkbox",
|
|
6942
|
+
tick: "checkbox",
|
|
6943
|
+
yesno: "checkbox",
|
|
6944
|
+
date: "datetime",
|
|
6945
|
+
timestamp: "datetime",
|
|
6946
|
+
datetime: "datetime",
|
|
6947
|
+
time: "datetime",
|
|
6948
|
+
dropdown: "select",
|
|
6949
|
+
enum: "select",
|
|
6950
|
+
choice: "select",
|
|
6951
|
+
singleselect: "select",
|
|
6952
|
+
multiselect: "multi_select",
|
|
6953
|
+
tags: "multi_select",
|
|
6954
|
+
tel: "phone",
|
|
6955
|
+
telephone: "phone",
|
|
6956
|
+
mobile: "phone",
|
|
6957
|
+
mail: "email",
|
|
6958
|
+
emailaddress: "email",
|
|
6959
|
+
website: "url",
|
|
6960
|
+
link: "url",
|
|
6961
|
+
uri: "url"
|
|
6962
|
+
};
|
|
6963
|
+
function inferKind(label, key) {
|
|
6964
|
+
const s = `${key.replace(/_/g, " ")} ${label}`.toLowerCase();
|
|
6965
|
+
if (/\be-?mail\b/.test(s)) return "email";
|
|
6966
|
+
if (/\b(phone|mobile|cell|telephone|fax)\b/.test(s)) return "phone";
|
|
6967
|
+
if (/\b(url|website|web site|link)\b/.test(s)) return "url";
|
|
6968
|
+
if (/\b(amount|fee|price|cost|total|balance|budget|salary|wage|deposit|revenue|payment)\b/.test(s)) return "currency";
|
|
6969
|
+
if (/\b(date|when|due|deadline|birthday|dob|start|end|time|at)\b/.test(s) || /_(on|at)$/.test(key)) return "datetime";
|
|
6970
|
+
if (/\b(notes?|description|details|comments?|summary|instructions)\b/.test(s)) return "long_text";
|
|
6971
|
+
if (/\b(count|quantity|qty|hours|score|age|number of|size|weight|rating)\b/.test(s)) return "number";
|
|
6972
|
+
if (/\b(name|title|owner|label|code|id|reference)\b/.test(s)) return "text";
|
|
6973
|
+
return void 0;
|
|
6974
|
+
}
|
|
6975
|
+
var wordOf = (v) => {
|
|
6976
|
+
if (typeof v !== "string") return void 0;
|
|
6977
|
+
if (RESOLVABLE_FIELD_TYPE_WORDS.includes(v)) return v;
|
|
6978
|
+
const syn = KIND_SYNONYMS[squash2(v)];
|
|
6979
|
+
return syn && RESOLVABLE_FIELD_TYPE_WORDS.includes(syn) ? syn : void 0;
|
|
6980
|
+
};
|
|
6981
|
+
function fixFieldKinds(spec, fixes) {
|
|
6982
|
+
for (const t of spec.tables) {
|
|
6983
|
+
for (const f of t.fields) {
|
|
6984
|
+
const raw = f;
|
|
6985
|
+
if (typeof raw.parityType === "string" && RESOLVABLE_FIELD_TYPE_WORDS.includes(raw.parityType)) continue;
|
|
6986
|
+
const at = `tables.${t.token}.fields.${f.key}`;
|
|
6987
|
+
const alias = KIND_KEYS.find((k) => raw[k] != null && typeof raw[k] === "string");
|
|
6988
|
+
let kind = wordOf(raw.parityType) ?? (alias ? wordOf(raw[alias]) : void 0);
|
|
6989
|
+
let how = alias ? `its "${alias}" says "${raw[alias]}"` : raw.parityType ? `"${raw.parityType}" is not a type word` : "it names no type";
|
|
6990
|
+
if (!kind && !alias && !raw.parityType) {
|
|
6991
|
+
kind = inferKind(String(f.label ?? ""), String(f.key ?? ""));
|
|
6992
|
+
how = `it names no type; "${f.label}" plainly is one`;
|
|
6993
|
+
}
|
|
6994
|
+
if (!kind) continue;
|
|
6995
|
+
for (const k of KIND_KEYS) if (k !== "parityType" && raw[k] === raw[alias ?? ""] && alias) delete raw[k];
|
|
6996
|
+
f.parityType = kind;
|
|
6997
|
+
if ((kind === "phone" || kind === "email") && !raw.format) raw.format = kind;
|
|
6998
|
+
fixes.push(`${at}: type ${kind} (${how})`);
|
|
6999
|
+
}
|
|
7000
|
+
}
|
|
7001
|
+
}
|
|
7002
|
+
function fixFormulaRefs(spec, fixes) {
|
|
7003
|
+
for (const t of spec.tables) {
|
|
7004
|
+
for (const f of t.fields) {
|
|
7005
|
+
if (f.parityType !== "formula" || typeof f.formula !== "string") continue;
|
|
7006
|
+
const others = t.fields.filter((x) => x.key !== f.key);
|
|
7007
|
+
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;
|
|
7008
|
+
const text = f.formula.replace(/^\s*=\s*/, "");
|
|
7009
|
+
let out = "";
|
|
7010
|
+
let i = 0;
|
|
7011
|
+
while (i < text.length) {
|
|
7012
|
+
const c = text[i];
|
|
7013
|
+
if (c === "{") {
|
|
7014
|
+
const j = text.indexOf("}", i);
|
|
7015
|
+
if (j < 0) {
|
|
7016
|
+
out += text.slice(i);
|
|
7017
|
+
break;
|
|
7018
|
+
}
|
|
7019
|
+
const name = text.slice(i + 1, j).trim();
|
|
7020
|
+
out += `{${labelFor(name) ?? name}}`;
|
|
7021
|
+
i = j + 1;
|
|
7022
|
+
} else if (c === "'" || c === '"') {
|
|
7023
|
+
const j = text.indexOf(c, i + 1);
|
|
7024
|
+
if (j < 0) {
|
|
7025
|
+
out += text.slice(i);
|
|
7026
|
+
break;
|
|
7027
|
+
}
|
|
7028
|
+
out += text.slice(i, j + 1);
|
|
7029
|
+
i = j + 1;
|
|
7030
|
+
} else if (/[A-Za-z_]/.test(c)) {
|
|
7031
|
+
const m = /^[A-Za-z_][A-Za-z_0-9]*/.exec(text.slice(i))[0];
|
|
7032
|
+
const after = text.slice(i + m.length).trimStart()[0];
|
|
7033
|
+
const label = after === "(" ? void 0 : labelFor(m);
|
|
7034
|
+
out += label ? `{${label}}` : m;
|
|
7035
|
+
i += m.length;
|
|
7036
|
+
} else {
|
|
7037
|
+
out += c;
|
|
7038
|
+
i += 1;
|
|
7039
|
+
}
|
|
7040
|
+
}
|
|
7041
|
+
if (out !== f.formula) {
|
|
7042
|
+
fixes.push(`tables.${t.token}.fields.${f.key}.formula: fields named by label in braces ("${f.formula}" \u2192 "${out}")`);
|
|
7043
|
+
f.formula = out;
|
|
7044
|
+
}
|
|
7045
|
+
}
|
|
7046
|
+
}
|
|
7047
|
+
}
|
|
7048
|
+
function fixInverseKeys(spec, fixes) {
|
|
7049
|
+
const rels = spec.relationships ?? [];
|
|
7050
|
+
const taken = /* @__PURE__ */ new Set();
|
|
7051
|
+
for (const t of allTables(spec)) for (const f of t.fields) taken.add(`${t.token}.${f.key}`);
|
|
7052
|
+
for (const r of rels) if (r.inverseKey) taken.add(`${r.toTable}.${r.inverseKey}`);
|
|
7053
|
+
for (const r of rels) {
|
|
7054
|
+
if (!r.inverseKey) continue;
|
|
7055
|
+
const target = tableOf(spec, r.toTable);
|
|
7056
|
+
if (!target?.fields.some((f) => f.key === r.inverseKey)) continue;
|
|
7057
|
+
let k = `${r.fromTable}_${r.fromField}_back`;
|
|
7058
|
+
for (let n = 2; taken.has(`${r.toTable}.${k}`); n++) k = `${r.fromTable}_${r.fromField}_back_${n}`;
|
|
7059
|
+
fixes.push(`relationships: ${r.fromTable}.${r.fromField} \u2192 ${r.toTable}: inverseKey "${r.inverseKey}" is already a field of ${r.toTable}; renamed "${k}"`);
|
|
7060
|
+
taken.add(`${r.toTable}.${k}`);
|
|
7061
|
+
r.inverseKey = k;
|
|
7062
|
+
}
|
|
7063
|
+
}
|
|
7064
|
+
var STATUS_WORDS = /\b(open|booked|upcoming|unpaid|outstanding|active|ordered|pending|overdue|scheduled|confirmed|paid|completed|current|remaining)\b/i;
|
|
7065
|
+
function fixRollupFilters(spec, fixes) {
|
|
7066
|
+
for (const t of spec.tables) {
|
|
7067
|
+
for (const f of t.fields) {
|
|
7068
|
+
if (f.parityType !== "rollup" || f.filter && Object.keys(f.filter).length || !f.via) continue;
|
|
7069
|
+
const hit = STATUS_WORDS.exec(`${f.label} ${f.key.replace(/_/g, " ")}`);
|
|
7070
|
+
const via = t.fields.find((x) => x.key === f.via);
|
|
7071
|
+
const far = via?.relationTarget ? tableOf(spec, via.relationTarget) : void 0;
|
|
7072
|
+
if (!hit || !far) continue;
|
|
7073
|
+
const word = hit[1].toLowerCase();
|
|
7074
|
+
const cands = [];
|
|
7075
|
+
for (const x of far.fields) {
|
|
7076
|
+
if (["select", "multi_select"].includes(x.parityType)) {
|
|
7077
|
+
for (const c of x.choices ?? []) if (c.toLowerCase() === word) cands.push({ key: x.key, value: c });
|
|
7078
|
+
}
|
|
7079
|
+
if (x.parityType === "checkbox" && new RegExp(`(^|_)${word}($|_)`).test(x.key)) cands.push({ key: x.key, value: true });
|
|
7080
|
+
if (x.parityType === "checkbox" && word === "unpaid" && /(^|_)paid($|_)/.test(x.key)) cands.push({ key: x.key, value: false });
|
|
7081
|
+
}
|
|
7082
|
+
if (cands.length !== 1) continue;
|
|
7083
|
+
f.filter = { [cands[0].key]: cands[0].value };
|
|
7084
|
+
fixes.push(`tables.${t.token}.fields.${f.key}: "${hit[1]}" in its name now picks those records: filter ${JSON.stringify(f.filter)}`);
|
|
7085
|
+
}
|
|
7086
|
+
}
|
|
7087
|
+
}
|
|
7088
|
+
function fictional(p) {
|
|
7089
|
+
const h = [...p].reduce((n, ch) => (n * 31 + ch.charCodeAt(0)) % 1e3, 7);
|
|
7090
|
+
const nn = String(h % 100).padStart(2, "0");
|
|
7091
|
+
const uk = String(h).padStart(3, "0");
|
|
7092
|
+
if (/^\s*\+/.test(p) && !/^\s*\+1\b/.test(p)) return `+44 20 7946 0${uk}`;
|
|
7093
|
+
if (/^\s*0\d/.test(p)) return `020 7946 0${uk}`;
|
|
7094
|
+
return p.replace(/(\d{3})([\s.-]?)(\d{4})\s*$/, (_m, _a, sep) => `555${sep}01${nn}`);
|
|
7095
|
+
}
|
|
7096
|
+
function fixFictionalPhones(spec, fixes) {
|
|
7097
|
+
const json = JSON.stringify(spec);
|
|
7098
|
+
const hits = /* @__PURE__ */ new Set();
|
|
7099
|
+
const isPhoneField = (f) => f.parityType === "phone" || /(^|_)(phone|mobile|cell|fax|tel)(_|$)/.test(f.key);
|
|
7100
|
+
const biz = spec.business?.phone;
|
|
7101
|
+
if (typeof biz === "string") for (const p of phonesOutsideFictionalRange(biz, true)) hits.add(p);
|
|
7102
|
+
for (const t of spec.tables) for (const r of t.rows ?? []) for (const f of t.fields) {
|
|
7103
|
+
const v = r.values?.[f.key];
|
|
7104
|
+
if (typeof v === "string") for (const p of phonesOutsideFictionalRange(v, isPhoneField(f))) hits.add(p);
|
|
7105
|
+
}
|
|
7106
|
+
for (const p of phonesOutsideFictionalRange(json)) hits.add(p);
|
|
7107
|
+
let out = json;
|
|
7108
|
+
let n = 0;
|
|
7109
|
+
for (const p of hits) {
|
|
7110
|
+
const escaped = JSON.stringify(p).slice(1, -1);
|
|
7111
|
+
if (!out.includes(escaped)) continue;
|
|
7112
|
+
out = out.split(escaped).join(JSON.stringify(fictional(p)).slice(1, -1));
|
|
7113
|
+
n += 1;
|
|
7114
|
+
}
|
|
7115
|
+
if (!n) return;
|
|
7116
|
+
const next = JSON.parse(out);
|
|
7117
|
+
for (const k of Object.keys(spec)) delete spec[k];
|
|
7118
|
+
Object.assign(spec, next);
|
|
7119
|
+
fixes.push(`phones: ${n} number(s) that could reach a real phone replaced with fictional ones (555-0100\u20260199, UK Ofcom drama range)`);
|
|
7120
|
+
}
|
|
7121
|
+
function fixTeamBlockCopy(spec, fixes) {
|
|
7122
|
+
const block = SHARED_BLOCKS.team_member;
|
|
7123
|
+
const used = /* @__PURE__ */ new Set();
|
|
7124
|
+
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]}`);
|
|
7125
|
+
for (const fm of spec.forms ?? []) for (const q2 of fm.fields ?? []) used.add(`${fm.table}.${q2.fieldKey}`);
|
|
7126
|
+
for (const e of spec.extras ?? []) for (const q2 of e.questions ?? []) used.add(`${e.table}.${q2.fieldKey}`);
|
|
7127
|
+
for (const t of spec.tables) {
|
|
7128
|
+
const meaning = (f) => block.fields.find((bf) => bf.parityType === f.parityType && bf.sameAs.some((w) => same3(w, f.label) || same3(w, f.key)));
|
|
7129
|
+
const hits = t.fields.map((f) => ({ f, bf: meaning(f) })).filter((x) => x.bf);
|
|
7130
|
+
if (hits.length < block.copyThreshold) continue;
|
|
7131
|
+
const hired = hits.some((h) => h.bf.key === "start_date");
|
|
7132
|
+
const hasRole = hits.some((h) => h.bf.key === "role");
|
|
7133
|
+
const contactOfSomeone = t.fields.some((f) => f.parityType === "relation" && f.relationTarget && f.relationTarget !== block.table && spec.tables.some((o) => o.token === f.relationTarget));
|
|
7134
|
+
if (!(hired || hasRole && !contactOfSomeone)) continue;
|
|
7135
|
+
const order = ["full_name", "role", "work_email", "mobile", "start_date"];
|
|
7136
|
+
const keep = new Set(hits.filter((h) => ["full_name", "role"].includes(h.bf.key)).map((h) => h.f.key));
|
|
7137
|
+
const dropped = [];
|
|
7138
|
+
let left = hits.length;
|
|
7139
|
+
for (const h of [...hits].sort((a, b) => order.indexOf(b.bf.key) - order.indexOf(a.bf.key))) {
|
|
7140
|
+
if (left < block.copyThreshold) break;
|
|
7141
|
+
if (keep.has(h.f.key) || used.has(`${t.token}.${h.f.key}`) || t.titleField === h.f.key) continue;
|
|
7142
|
+
dropped.push(h.f.label);
|
|
7143
|
+
t.fields = t.fields.filter((x) => x.key !== h.f.key);
|
|
7144
|
+
for (const r of t.rows ?? []) if (r.values) delete r.values[h.f.key];
|
|
7145
|
+
left -= 1;
|
|
7146
|
+
}
|
|
7147
|
+
if (left >= block.copyThreshold) continue;
|
|
7148
|
+
if (!t.fields.some((f) => f.key === "notes")) t.fields.push({ key: "notes", label: "Notes", parityType: "long_text", sensitivity: "internal", contextPolicy: "include" });
|
|
7149
|
+
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"}`);
|
|
7150
|
+
}
|
|
7151
|
+
}
|
|
7152
|
+
|
|
6657
7153
|
// src/templates/describe/autofix.ts
|
|
6658
7154
|
var clone2 = (v) => JSON.parse(JSON.stringify(v));
|
|
6659
7155
|
var SENSITIVITY_RANK2 = ["public", "internal", "confidential", "restricted"];
|
|
@@ -6975,6 +7471,15 @@ function switchJob(spec, fixes) {
|
|
|
6975
7471
|
function autofixDescribeSpec(input, ctx = {}) {
|
|
6976
7472
|
const spec = describeSpec(clone2(input));
|
|
6977
7473
|
const autoFixes = [];
|
|
7474
|
+
fixFieldKinds(spec, autoFixes);
|
|
7475
|
+
normalizeComputedFields(spec, autoFixes);
|
|
7476
|
+
fixFormulaRefs(spec, autoFixes);
|
|
7477
|
+
fixViewShapes(spec, autoFixes);
|
|
7478
|
+
dropDuplicateViews(spec, autoFixes);
|
|
7479
|
+
fixInverseKeys(spec, autoFixes);
|
|
7480
|
+
fixRollupFilters(spec, autoFixes);
|
|
7481
|
+
fixTeamBlockCopy(spec, autoFixes);
|
|
7482
|
+
fixFictionalPhones(spec, autoFixes);
|
|
6978
7483
|
fixMissingQuestionFields(spec, autoFixes);
|
|
6979
7484
|
raiseSensitivity(spec, autoFixes);
|
|
6980
7485
|
flagAskedOfSubject(spec, ctx, autoFixes);
|
|
@@ -7198,6 +7703,30 @@ var VIEW_SHAPE_FACTS = {
|
|
|
7198
7703
|
startField_endField: "timeline only, required: keys of two datetime fields",
|
|
7199
7704
|
coverField: "gallery only, required: the key of a file field"
|
|
7200
7705
|
};
|
|
7706
|
+
var ROLLUP_SHAPE_FACTS = {
|
|
7707
|
+
rule: 'a rollup is a field of the PARENT table that adds up the records linked to it: parityType "rollup", via = the key of a relation field of THIS table (that link must be a list: multi true), pick = the key of a field on the far table, agg = sum | count | avg | min | max; an optional filter picks which linked records. Write exactly these keys on the field itself \u2014 never a nested {rollup:{\u2026}} object, never relation/field/fn.',
|
|
7708
|
+
example: { key: "total_paid", label: "Total paid", parityType: "rollup", via: "payments", pick: "amount", agg: "sum" }
|
|
7709
|
+
};
|
|
7710
|
+
var LOOKUP_SHAPE_FACTS = {
|
|
7711
|
+
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.',
|
|
7712
|
+
example: { key: "client_phone", label: "Client phone", parityType: "lookup", via: "client", pick: "phone" }
|
|
7713
|
+
};
|
|
7714
|
+
var VIEW_FILTERS_SHAPE_FACTS = {
|
|
7715
|
+
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.",
|
|
7716
|
+
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" }] }
|
|
7717
|
+
};
|
|
7718
|
+
var FIELD_SHAPE_FACTS = {
|
|
7719
|
+
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.",
|
|
7720
|
+
example: { key: "visit_fee", label: "Visit fee", parityType: "currency", sensitivity: "internal", contextPolicy: "include" }
|
|
7721
|
+
};
|
|
7722
|
+
var FORMULA_SHAPE_FACTS = {
|
|
7723
|
+
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.",
|
|
7724
|
+
example: { key: "line_total", label: "Line total", parityType: "formula", formula: "{Quantity} * {Unit price}" }
|
|
7725
|
+
};
|
|
7726
|
+
var RELATIONSHIP_SHAPE_FACTS = {
|
|
7727
|
+
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.",
|
|
7728
|
+
example: { fromTable: "request", fromField: "client", toTable: "client", flavor: "referenced", cardinality: "one", onDelete: "restrict", inverseKey: "requests_from" }
|
|
7729
|
+
};
|
|
7201
7730
|
function describeVocabulary() {
|
|
7202
7731
|
return {
|
|
7203
7732
|
field_kinds: RESOLVABLE_FIELD_TYPE_WORDS,
|
|
@@ -7221,6 +7750,14 @@ function describeVocabulary() {
|
|
|
7221
7750
|
booking_window: { weekday: "0 = Sunday \u2026 6 = Saturday", from: "HH:MM", to: "HH:MM later than from" },
|
|
7222
7751
|
booking_availability: BOOKING_AVAILABILITY_FACTS,
|
|
7223
7752
|
view_shape: VIEW_SHAPE_FACTS,
|
|
7753
|
+
rollup_shape: ROLLUP_SHAPE_FACTS,
|
|
7754
|
+
lookup_shape: LOOKUP_SHAPE_FACTS,
|
|
7755
|
+
view_filters_shape: VIEW_FILTERS_SHAPE_FACTS,
|
|
7756
|
+
field_shape: FIELD_SHAPE_FACTS,
|
|
7757
|
+
formula_shape: FORMULA_SHAPE_FACTS,
|
|
7758
|
+
relationship_shape: RELATIONSHIP_SHAPE_FACTS,
|
|
7759
|
+
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.",
|
|
7760
|
+
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.",
|
|
7224
7761
|
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"
|
|
7225
7762
|
};
|
|
7226
7763
|
}
|