@ai-matrx/records 0.76.8 → 0.76.10
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 +12 -0
- package/dist/app-table/index.cjs +2 -1
- package/dist/app-table/index.cjs.map +1 -1
- package/dist/app-table/index.js +2 -1
- package/dist/app-table/index.js.map +1 -1
- package/dist/memory/index.cjs.map +1 -1
- package/dist/memory/index.d.cts +1 -1
- package/dist/memory/index.d.ts +1 -1
- package/dist/memory/index.js.map +1 -1
- package/dist/react/index.cjs +2 -1
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.js +2 -1
- package/dist/react/index.js.map +1 -1
- package/dist/templates/index.cjs +898 -151
- package/dist/templates/index.cjs.map +1 -1
- package/dist/templates/index.d.cts +265 -4
- package/dist/templates/index.d.ts +265 -4
- package/dist/templates/index.js +898 -151
- package/dist/templates/index.js.map +1 -1
- package/dist/{types-DlRG9iZh.d.cts → types-D9HN8YZS.d.cts} +1 -1
- package/dist/{types-bdEWPW4p.d.ts → types-DvhixdUB.d.ts} +1 -1
- package/package.json +8 -4
package/dist/templates/index.js
CHANGED
|
@@ -371,9 +371,9 @@ function membersAddRowsSteps(spec, tableRef) {
|
|
|
371
371
|
}));
|
|
372
372
|
}
|
|
373
373
|
function confidentialRequests(spec, made) {
|
|
374
|
-
const
|
|
374
|
+
const idOf2 = (token) => made.find((m) => m.kind === "table" && m.ref === `tables.${token}`)?.id ?? null;
|
|
375
375
|
return (spec.confidential ?? []).map((c) => {
|
|
376
|
-
const tableId =
|
|
376
|
+
const tableId = idOf2(c.table);
|
|
377
377
|
if (!tableId) throw new Error(`The install made no "${c.table}" table, so it cannot be made Confidential.`);
|
|
378
378
|
const readers = c.readers.map((r) => ({ field: r.field, level: r.level, ...r.when ? { when: whenRule(c.table, r.when, made) } : {} }));
|
|
379
379
|
return {
|
|
@@ -1479,6 +1479,7 @@ var TABLE_TYPES = [
|
|
|
1479
1479
|
var RELATION_FLAVORS = ["owned", "referenced"];
|
|
1480
1480
|
var ON_DELETE = ["cascade", "set_null", "restrict"];
|
|
1481
1481
|
var RELATION_CARDINALITIES = ["one", "many"];
|
|
1482
|
+
var FIELD_SOURCES = ["manual", "formula", "agent", "synced"];
|
|
1482
1483
|
var FIELD_SENSITIVITIES = ["public", "internal", "confidential", "restricted"];
|
|
1483
1484
|
var CONTEXT_POLICIES = ["include", "summarize", "exclude", "on_request"];
|
|
1484
1485
|
var TABLE_DISPLAYS = ["list", "grid", "board", "calendar", "timeline"];
|
|
@@ -1825,39 +1826,39 @@ function validateUseCase(useCase) {
|
|
|
1825
1826
|
`The title field "${table.titleField}" is not one of this table's fields \u2014 without it a record cannot be a chip.`
|
|
1826
1827
|
);
|
|
1827
1828
|
}
|
|
1828
|
-
table.fields.forEach((
|
|
1829
|
-
const fat = `${at}.fields[${fi}] (${
|
|
1830
|
-
if (!/^[a-z][a-z0-9_]*$/.test(
|
|
1831
|
-
say(`${fat}.key`, `"${
|
|
1829
|
+
table.fields.forEach((field2, fi) => {
|
|
1830
|
+
const fat = `${at}.fields[${fi}] (${field2.key})`;
|
|
1831
|
+
if (!/^[a-z][a-z0-9_]*$/.test(field2.key ?? "")) {
|
|
1832
|
+
say(`${fat}.key`, `"${field2.key}" should be snake_case and start with a letter.`);
|
|
1832
1833
|
}
|
|
1833
|
-
if (!RESOLVABLE_FIELD_TYPE_WORDS.includes(
|
|
1834
|
+
if (!RESOLVABLE_FIELD_TYPE_WORDS.includes(field2.parityType)) {
|
|
1834
1835
|
say(
|
|
1835
1836
|
`${fat}.parityType`,
|
|
1836
|
-
`The store does not resolve the type word "${
|
|
1837
|
+
`The store does not resolve the type word "${field2.parityType}". Use one of: ${RESOLVABLE_FIELD_TYPE_WORDS.join(", ")}.`
|
|
1837
1838
|
);
|
|
1838
1839
|
}
|
|
1839
|
-
if (!FIELD_SENSITIVITIES.includes(
|
|
1840
|
+
if (!FIELD_SENSITIVITIES.includes(field2.sensitivity)) {
|
|
1840
1841
|
say(
|
|
1841
1842
|
`${fat}.sensitivity`,
|
|
1842
1843
|
`The store refuses a field that does not say how sensitive its values are. Use one of: ${FIELD_SENSITIVITIES.join(", ")}.`
|
|
1843
1844
|
);
|
|
1844
1845
|
}
|
|
1845
|
-
if (!CONTEXT_POLICIES.includes(
|
|
1846
|
+
if (!CONTEXT_POLICIES.includes(field2.contextPolicy)) {
|
|
1846
1847
|
say(
|
|
1847
1848
|
`${fat}.contextPolicy`,
|
|
1848
1849
|
`The store refuses a field that does not say whether an agent may see its values. Use one of: ${CONTEXT_POLICIES.join(", ")}.`
|
|
1849
1850
|
);
|
|
1850
1851
|
}
|
|
1851
|
-
if (CHOICE_TYPES.has(
|
|
1852
|
+
if (CHOICE_TYPES.has(field2.parityType) && !field2.choices?.length) {
|
|
1852
1853
|
say(
|
|
1853
1854
|
`${fat}.choices`,
|
|
1854
1855
|
"A choice field has to list the business's real words, in the order they really happen \u2014 'Scheduled, Confirmed, Seen, No-show'."
|
|
1855
1856
|
);
|
|
1856
1857
|
}
|
|
1857
|
-
if (RELATION_TYPES.has(
|
|
1858
|
-
say(`${fat}.relationTarget`, `It points at the table "${
|
|
1858
|
+
if (RELATION_TYPES.has(field2.parityType) && field2.relationTarget && !tablesByToken.has(field2.relationTarget)) {
|
|
1859
|
+
say(`${fat}.relationTarget`, `It points at the table "${field2.relationTarget}", which this use case does not declare.`);
|
|
1859
1860
|
}
|
|
1860
|
-
if (
|
|
1861
|
+
if (field2.parityType === "currency" && !field2.unit) {
|
|
1861
1862
|
say(`${fat}.unit`, "A currency field carries its currency as the unit \u2014 the unit changes the meaning.");
|
|
1862
1863
|
}
|
|
1863
1864
|
});
|
|
@@ -1872,16 +1873,16 @@ function validateUseCase(useCase) {
|
|
|
1872
1873
|
say(`${rat}.values.${valueKey}`, `This row sets "${valueKey}", which the table does not declare as a field.`);
|
|
1873
1874
|
}
|
|
1874
1875
|
}
|
|
1875
|
-
for (const
|
|
1876
|
-
if (
|
|
1877
|
-
say(`${rat}.values.${
|
|
1876
|
+
for (const field2 of table.fields) {
|
|
1877
|
+
if (field2.required && (row.values?.[field2.key] === void 0 || row.values?.[field2.key] === null)) {
|
|
1878
|
+
say(`${rat}.values.${field2.key}`, `"${field2.label}" is required, and this row leaves it empty.`);
|
|
1878
1879
|
}
|
|
1879
|
-
const value = row.values?.[
|
|
1880
|
-
if (value !== void 0 &&
|
|
1881
|
-
if (typeof value === "string" && !
|
|
1880
|
+
const value = row.values?.[field2.key];
|
|
1881
|
+
if (value !== void 0 && field2.choices?.length && !field2.multi) {
|
|
1882
|
+
if (typeof value === "string" && !field2.choices.includes(value)) {
|
|
1882
1883
|
say(
|
|
1883
|
-
`${rat}.values.${
|
|
1884
|
-
`"${value}" is not one of "${
|
|
1884
|
+
`${rat}.values.${field2.key}`,
|
|
1885
|
+
`"${value}" is not one of "${field2.label}"'s choices (${field2.choices.join(", ")}).`
|
|
1885
1886
|
);
|
|
1886
1887
|
}
|
|
1887
1888
|
}
|
|
@@ -1905,18 +1906,18 @@ function validateUseCase(useCase) {
|
|
|
1905
1906
|
if (!tablesByToken.has(rel.toTable)) {
|
|
1906
1907
|
say(`${at}.toTable`, `There is no table "${rel.toTable}" in this use case.`);
|
|
1907
1908
|
}
|
|
1908
|
-
const
|
|
1909
|
-
if (!
|
|
1909
|
+
const field2 = resolveField(rel.fromTable, rel.fromField);
|
|
1910
|
+
if (!field2) {
|
|
1910
1911
|
say(`${at}.fromField`, `The table "${rel.fromTable}" does not declare a field "${rel.fromField}".`);
|
|
1911
|
-
} else if (!RELATION_TYPES.has(
|
|
1912
|
+
} else if (!RELATION_TYPES.has(field2.parityType)) {
|
|
1912
1913
|
say(
|
|
1913
1914
|
`${at}.fromField`,
|
|
1914
|
-
`"${rel.fromField}" is a ${
|
|
1915
|
+
`"${rel.fromField}" is a ${field2.parityType} field, so it cannot hold a relation. Declare it as a relation.`
|
|
1915
1916
|
);
|
|
1916
|
-
} else if (
|
|
1917
|
+
} else if (field2.relationTarget && field2.relationTarget !== rel.toTable) {
|
|
1917
1918
|
say(
|
|
1918
1919
|
`${at}.fromField`,
|
|
1919
|
-
`The field points at "${
|
|
1920
|
+
`The field points at "${field2.relationTarget}" but the relationship says "${rel.toTable}". They have to agree.`
|
|
1920
1921
|
);
|
|
1921
1922
|
}
|
|
1922
1923
|
if (!RELATION_FLAVORS.includes(rel.flavor)) {
|
|
@@ -2074,12 +2075,12 @@ var ACTOR = "user";
|
|
|
2074
2075
|
var COMPUTED = /* @__PURE__ */ new Set(["lookup", "rollup", "formula"]);
|
|
2075
2076
|
var isTimedField = (f) => f.parityType === "datetime" && f.format === "datetime";
|
|
2076
2077
|
var isSelfDeclaredField = (f) => f.parityType !== "relation" && !COMPUTED.has(f.parityType) && (isTimedField(f) || f.default !== void 0 || f.parityType === "entity_reference");
|
|
2077
|
-
function choiceDisplayFormat(
|
|
2078
|
-
if (!
|
|
2078
|
+
function choiceDisplayFormat(field2) {
|
|
2079
|
+
if (!field2.choices?.length || !field2.choiceColors) return void 0;
|
|
2079
2080
|
return {
|
|
2080
|
-
id:
|
|
2081
|
+
id: field2.parityType === "multi_select" ? "multi_choice" : "choice",
|
|
2081
2082
|
options: {
|
|
2082
|
-
choices:
|
|
2083
|
+
choices: field2.choices.filter((c) => field2.choiceColors?.[c]).map((c) => ({ value: c, color: field2.choiceColors[c] }))
|
|
2083
2084
|
}
|
|
2084
2085
|
};
|
|
2085
2086
|
}
|
|
@@ -2147,49 +2148,49 @@ function shellsPayload(spec, tables, deferred = deferredRelations(tables)) {
|
|
|
2147
2148
|
})
|
|
2148
2149
|
};
|
|
2149
2150
|
}
|
|
2150
|
-
function timedFieldSpec(
|
|
2151
|
+
function timedFieldSpec(field2) {
|
|
2151
2152
|
return {
|
|
2152
|
-
key:
|
|
2153
|
-
label:
|
|
2153
|
+
key: field2.key,
|
|
2154
|
+
label: field2.label,
|
|
2154
2155
|
type: "datetime",
|
|
2155
2156
|
kind: "datetime",
|
|
2156
|
-
...
|
|
2157
|
-
...
|
|
2158
|
-
sensitivity:
|
|
2159
|
-
context_policy:
|
|
2157
|
+
...field2.required ? { required: true } : {},
|
|
2158
|
+
...field2.rules ? { rules: field2.rules } : {},
|
|
2159
|
+
sensitivity: field2.sensitivity,
|
|
2160
|
+
context_policy: field2.contextPolicy
|
|
2160
2161
|
};
|
|
2161
2162
|
}
|
|
2162
|
-
function selfDeclaredFieldSpec(
|
|
2163
|
-
if (isTimedField(
|
|
2164
|
-
const allowed =
|
|
2165
|
-
const displayFormat = choiceDisplayFormat(
|
|
2163
|
+
function selfDeclaredFieldSpec(field2) {
|
|
2164
|
+
if (isTimedField(field2)) return { ...timedFieldSpec(field2), ...field2.default !== void 0 ? { default: field2.default } : {} };
|
|
2165
|
+
const allowed = field2.parityType === "entity_reference" ? { allowed_types: field2.allowedTypes ?? [] } : {};
|
|
2166
|
+
const displayFormat = choiceDisplayFormat(field2);
|
|
2166
2167
|
return {
|
|
2167
|
-
key:
|
|
2168
|
-
label:
|
|
2169
|
-
type:
|
|
2170
|
-
...
|
|
2171
|
-
...
|
|
2172
|
-
...
|
|
2173
|
-
...
|
|
2174
|
-
...
|
|
2168
|
+
key: field2.key,
|
|
2169
|
+
label: field2.label,
|
|
2170
|
+
type: field2.parityType,
|
|
2171
|
+
...field2.multi ? { multi: true } : {},
|
|
2172
|
+
...field2.required ? { required: true } : {},
|
|
2173
|
+
...field2.unit ? { unit: field2.unit } : {},
|
|
2174
|
+
...field2.rules ? { rules: field2.rules } : {},
|
|
2175
|
+
...field2.choices ? { options: field2.choices } : {},
|
|
2175
2176
|
...displayFormat ? { display_format: displayFormat } : {},
|
|
2176
|
-
...
|
|
2177
|
+
...field2.default !== void 0 ? { default: field2.default } : {},
|
|
2177
2178
|
...allowed,
|
|
2178
|
-
sensitivity:
|
|
2179
|
-
context_policy:
|
|
2179
|
+
sensitivity: field2.sensitivity,
|
|
2180
|
+
context_policy: field2.contextPolicy
|
|
2180
2181
|
};
|
|
2181
2182
|
}
|
|
2182
|
-
function relationFieldSpec(
|
|
2183
|
+
function relationFieldSpec(field2, targetTableId) {
|
|
2183
2184
|
return {
|
|
2184
|
-
key:
|
|
2185
|
-
label:
|
|
2185
|
+
key: field2.key,
|
|
2186
|
+
label: field2.label,
|
|
2186
2187
|
type: "relation",
|
|
2187
2188
|
relation_target: targetTableId,
|
|
2188
|
-
...
|
|
2189
|
-
...
|
|
2190
|
-
...
|
|
2191
|
-
sensitivity:
|
|
2192
|
-
context_policy:
|
|
2189
|
+
...field2.multi ? { multi: true } : {},
|
|
2190
|
+
...field2.relationMax ? { relation_max: field2.relationMax } : {},
|
|
2191
|
+
...field2.required ? { required: true } : {},
|
|
2192
|
+
sensitivity: field2.sensitivity,
|
|
2193
|
+
context_policy: field2.contextPolicy
|
|
2193
2194
|
};
|
|
2194
2195
|
}
|
|
2195
2196
|
function rowLevels(tables) {
|
|
@@ -2249,12 +2250,12 @@ function relationPatches(table, ids, deferred) {
|
|
|
2249
2250
|
for (const [key, target] of rel) {
|
|
2250
2251
|
const v = r.values?.[key];
|
|
2251
2252
|
if (v === null || v === void 0) continue;
|
|
2252
|
-
const
|
|
2253
|
+
const idOf2 = (k) => {
|
|
2253
2254
|
const id = ids.get(`${target}.${String(k)}`);
|
|
2254
2255
|
if (!id) throw new Error(`A row points at "${String(k)}" in "${target}", which the template does not have.`);
|
|
2255
2256
|
return id;
|
|
2256
2257
|
};
|
|
2257
|
-
patch[key] = Array.isArray(v) ? v.map(
|
|
2258
|
+
patch[key] = Array.isArray(v) ? v.map(idOf2) : idOf2(v);
|
|
2258
2259
|
}
|
|
2259
2260
|
if (Object.keys(patch).length) out.push({ op: "update", record_id: ids.get(`${table.token}.${r.key}`), patch: { ...patch, _actor: ACTOR } });
|
|
2260
2261
|
}
|
|
@@ -2263,7 +2264,7 @@ function relationPatches(table, ids, deferred) {
|
|
|
2263
2264
|
function rowsFor(table, ids, deferred) {
|
|
2264
2265
|
const relation = new Map(table.fields.filter((f) => f.parityType === "relation").map((f) => [f.key, f]));
|
|
2265
2266
|
const derived = new Set(table.fields.filter((f) => COMPUTED.has(f.parityType)).map((f) => f.key));
|
|
2266
|
-
const
|
|
2267
|
+
const idOf2 = (target, key) => {
|
|
2267
2268
|
const id = ids.get(`${target}.${String(key)}`);
|
|
2268
2269
|
if (!id) throw new Error(`A row points at "${String(key)}" in "${target}", which the template does not have.`);
|
|
2269
2270
|
return id;
|
|
@@ -2277,7 +2278,7 @@ function rowsFor(table, ids, deferred) {
|
|
|
2277
2278
|
values[key] = value;
|
|
2278
2279
|
continue;
|
|
2279
2280
|
}
|
|
2280
|
-
values[key] = Array.isArray(value) ? value.map((v) =>
|
|
2281
|
+
values[key] = Array.isArray(value) ? value.map((v) => idOf2(rel.relationTarget, v)) : idOf2(rel.relationTarget, value);
|
|
2281
2282
|
}
|
|
2282
2283
|
if (typeof r.icon === "string" && r.icon.trim()) values["icon"] = r.icon.trim();
|
|
2283
2284
|
values["_actor"] = ACTOR;
|
|
@@ -2285,17 +2286,17 @@ function rowsFor(table, ids, deferred) {
|
|
|
2285
2286
|
});
|
|
2286
2287
|
return { rows, ids: table.rows.map((r) => ids.get(`${table.token}.${r.key}`)) };
|
|
2287
2288
|
}
|
|
2288
|
-
function derivedFieldSpec(
|
|
2289
|
+
function derivedFieldSpec(field2) {
|
|
2289
2290
|
const base = {
|
|
2290
|
-
key:
|
|
2291
|
-
label:
|
|
2292
|
-
type:
|
|
2293
|
-
via:
|
|
2294
|
-
sensitivity:
|
|
2295
|
-
context_policy:
|
|
2296
|
-
...
|
|
2291
|
+
key: field2.key,
|
|
2292
|
+
label: field2.label,
|
|
2293
|
+
type: field2.parityType,
|
|
2294
|
+
via: field2.via,
|
|
2295
|
+
sensitivity: field2.sensitivity,
|
|
2296
|
+
context_policy: field2.contextPolicy,
|
|
2297
|
+
...field2.unit ? { unit: field2.unit } : {}
|
|
2297
2298
|
};
|
|
2298
|
-
return
|
|
2299
|
+
return field2.parityType === "rollup" ? { ...base, agg: field2.agg, of: field2.pick, ...field2.filter ? { filter: field2.filter } : {} } : { ...base, pick: field2.pick };
|
|
2299
2300
|
}
|
|
2300
2301
|
function viewSpec(view, fieldIds = {}) {
|
|
2301
2302
|
const definition = { layout: view.kind };
|
|
@@ -2341,15 +2342,15 @@ function anyOfWhere(anyOf, fieldIds) {
|
|
|
2341
2342
|
})
|
|
2342
2343
|
};
|
|
2343
2344
|
}
|
|
2344
|
-
function formulaFieldSpec(
|
|
2345
|
+
function formulaFieldSpec(field2) {
|
|
2345
2346
|
return {
|
|
2346
|
-
key:
|
|
2347
|
-
label:
|
|
2347
|
+
key: field2.key,
|
|
2348
|
+
label: field2.label,
|
|
2348
2349
|
type: "formula",
|
|
2349
|
-
formula_text:
|
|
2350
|
-
...
|
|
2351
|
-
sensitivity:
|
|
2352
|
-
context_policy:
|
|
2350
|
+
formula_text: field2.formula,
|
|
2351
|
+
...field2.unit ? { unit: field2.unit } : {},
|
|
2352
|
+
sensitivity: field2.sensitivity,
|
|
2353
|
+
context_policy: field2.contextPolicy
|
|
2353
2354
|
};
|
|
2354
2355
|
}
|
|
2355
2356
|
function formulaOrder(table) {
|
|
@@ -2967,23 +2968,23 @@ var BUCKETS = ["year", "quarter", "month", "week", "day"];
|
|
|
2967
2968
|
var AGG_OPERATIONS = ["count", "sum", "avg", "min", "max", "median", "filled", "empty", "unique"];
|
|
2968
2969
|
var MEASURE_KEY = /^[a-zA-Z_][a-zA-Z0-9_]{0,62}$/;
|
|
2969
2970
|
var STORE_LIST_RELATION_MAX = 25;
|
|
2970
|
-
function ruleBreak(
|
|
2971
|
+
function ruleBreak(field2, v) {
|
|
2971
2972
|
const t = storeText(v) ?? "";
|
|
2972
2973
|
if (t === "") return null;
|
|
2973
|
-
if (
|
|
2974
|
-
for (const rule of
|
|
2974
|
+
if (field2.choices?.length && !field2.multi && typeof v === "string" && !field2.choices.includes(v)) return `"${v}" is not one of "${field2.label}"'s choices.`;
|
|
2975
|
+
for (const rule of field2.rules ?? []) {
|
|
2975
2976
|
if (rule.kind === "min" || rule.kind === "max") {
|
|
2976
2977
|
const n = parseNumeric(t);
|
|
2977
2978
|
const bound = parseNumeric(String(rule.value));
|
|
2978
|
-
if (n && bound && (rule.kind === "min" ? qCmp(n, bound) < 0 : qCmp(n, bound) > 0)) return `${t} breaks "${
|
|
2979
|
+
if (n && bound && (rule.kind === "min" ? qCmp(n, bound) < 0 : qCmp(n, bound) > 0)) return `${t} breaks "${field2.label}"'s rule: at ${rule.kind === "min" ? "least" : "most"} ${rule.value}.`;
|
|
2979
2980
|
} else if (rule.kind === "length") {
|
|
2980
|
-
if (rule.value !== void 0 && t.length > Number(rule.value)) return `"${t}" is longer than "${
|
|
2981
|
-
if (rule.min !== void 0 && t.length < rule.min) return `"${t}" is shorter than "${
|
|
2981
|
+
if (rule.value !== void 0 && t.length > Number(rule.value)) return `"${t}" is longer than "${field2.label}" allows (${rule.value}).`;
|
|
2982
|
+
if (rule.min !== void 0 && t.length < rule.min) return `"${t}" is shorter than "${field2.label}" allows (${rule.min}).`;
|
|
2982
2983
|
} else if (rule.kind === "pattern" && rule.value !== void 0) {
|
|
2983
2984
|
try {
|
|
2984
|
-
if (!new RegExp(String(rule.value)).test(t)) return `"${t}" does not match "${
|
|
2985
|
+
if (!new RegExp(String(rule.value)).test(t)) return `"${t}" does not match "${field2.label}"'s own pattern.`;
|
|
2985
2986
|
} catch {
|
|
2986
|
-
return `"${
|
|
2987
|
+
return `"${field2.label}"'s pattern is not a pattern.`;
|
|
2987
2988
|
}
|
|
2988
2989
|
}
|
|
2989
2990
|
}
|
|
@@ -3020,14 +3021,14 @@ function deriveSingulars(spec) {
|
|
|
3020
3021
|
if (form) out.form = form;
|
|
3021
3022
|
const stages = spec.extras?.find((e) => e.kind === "stage_rules");
|
|
3022
3023
|
if (stages?.kind === "stage_rules") {
|
|
3023
|
-
const
|
|
3024
|
+
const field2 = allTables(spec).find((t) => t.token === stages.table)?.fields.find((f) => f.key === stages.stageField);
|
|
3024
3025
|
out.pipeline = {
|
|
3025
3026
|
token: stages.token,
|
|
3026
3027
|
name: stages.name,
|
|
3027
3028
|
describes: stages.movesMessage ?? stages.name,
|
|
3028
3029
|
table: stages.table,
|
|
3029
3030
|
stageField: stages.stageField,
|
|
3030
|
-
stages: (
|
|
3031
|
+
stages: (field2?.choices ?? Object.keys(stages.stages ?? {})).map((key) => ({ key, label: key, describes: stages.stages?.[key] ?? "" }))
|
|
3031
3032
|
};
|
|
3032
3033
|
}
|
|
3033
3034
|
const q6 = spec.agent?.question;
|
|
@@ -3146,12 +3147,12 @@ function capabilitiesUsed(spec) {
|
|
|
3146
3147
|
}
|
|
3147
3148
|
if (spec.teaches === "gallery") used.add("seed.attachments");
|
|
3148
3149
|
for (const table of allTables(spec)) {
|
|
3149
|
-
for (const
|
|
3150
|
-
const word = `field.${
|
|
3150
|
+
for (const field2 of table.fields ?? []) {
|
|
3151
|
+
const word = `field.${field2.parityType}`;
|
|
3151
3152
|
if (CAPABILITY_WORDS.includes(word)) used.add(word);
|
|
3152
|
-
if (
|
|
3153
|
-
if (
|
|
3154
|
-
if (
|
|
3153
|
+
if (field2.choiceColors && Object.keys(field2.choiceColors).length) used.add("choice.colors");
|
|
3154
|
+
if (field2.format === "rating") used.add("field.rating");
|
|
3155
|
+
if (field2.format === "signature") used.add("field.signature");
|
|
3155
3156
|
}
|
|
3156
3157
|
}
|
|
3157
3158
|
if (spec.forms?.length) {
|
|
@@ -3362,9 +3363,9 @@ function foreignTextProblems(spec, library) {
|
|
|
3362
3363
|
const all = hay.map(([, v]) => v).join("\n");
|
|
3363
3364
|
const esc = (w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3364
3365
|
const find = (word) => {
|
|
3365
|
-
const
|
|
3366
|
-
if (!all.includes(
|
|
3367
|
-
const re = new RegExp(`(^|[^a-z0-9])${esc(
|
|
3366
|
+
const lower2 = word.toLowerCase();
|
|
3367
|
+
if (!all.includes(lower2)) return void 0;
|
|
3368
|
+
const re = new RegExp(`(^|[^a-z0-9])${esc(lower2)}($|[^a-z0-9])`);
|
|
3368
3369
|
return hay.find(([, v]) => re.test(v));
|
|
3369
3370
|
};
|
|
3370
3371
|
const mine = proseOf(spec).map(([at, w]) => {
|
|
@@ -3523,6 +3524,10 @@ function validateTemplate(raw, options = {}) {
|
|
|
3523
3524
|
}
|
|
3524
3525
|
for (const table of tablesRaw) {
|
|
3525
3526
|
const tf = table.fields.find((f) => f.key === table.titleField);
|
|
3527
|
+
if (!tf) say(`tables (${table.token}).titleField`, `The title field "${table.titleField}" is not one of this table's fields; name a plain text-like field of "${table.token}" (a name, a title) \u2014 the install refuses anything else.`);
|
|
3528
|
+
else if (isRelation(tf) || ["lookup", "rollup", "formula"].includes(tf.parityType) || isTimedField(tf) || tf.parityType === "entity_reference") {
|
|
3529
|
+
say(`tables (${table.token}).titleField`, `The title field "${tf.key}" is a ${isRelation(tf) ? "link to another table" : tf.parityType} column, which the store adds after the table is made; the title has to be a plain text-like field made with the table (a name or title). Add one, or point titleField at it.`);
|
|
3530
|
+
}
|
|
3526
3531
|
if (tf && tf.default !== void 0) say(`tables (${table.token}).fields (${tf.key})`, `"${tf.label}" names each ${table.labelSingular.toLowerCase()}, so it takes no default; ask it on the form instead.`);
|
|
3527
3532
|
}
|
|
3528
3533
|
for (const table of tablesRaw) {
|
|
@@ -3711,49 +3716,49 @@ function validateTemplate(raw, options = {}) {
|
|
|
3711
3716
|
let hasLookupOrRollup = false;
|
|
3712
3717
|
tables.forEach((table) => {
|
|
3713
3718
|
const tat = `tables (${table.token})`;
|
|
3714
|
-
table.fields.forEach((
|
|
3715
|
-
const at = `${tat}.fields (${
|
|
3716
|
-
if (isChoice(
|
|
3717
|
-
const colors =
|
|
3718
|
-
|
|
3719
|
+
table.fields.forEach((field2) => {
|
|
3720
|
+
const at = `${tat}.fields (${field2.key})`;
|
|
3721
|
+
if (isChoice(field2) && field2.choices?.length) {
|
|
3722
|
+
const colors = field2.choices.map((c) => field2.choiceColors?.[c]);
|
|
3723
|
+
field2.choices.forEach((c, i) => {
|
|
3719
3724
|
const color = colors[i];
|
|
3720
3725
|
if (!color) say(`${at}.choiceColors`, `The choice "${c}" wears no colour. Give every choice one of: ${CHOICE_COLORS.join(", ")}.`);
|
|
3721
3726
|
else if (!CHOICE_COLORS.includes(color)) say(`${at}.choiceColors`, `"${color}" is not a colour the grid can paint.`);
|
|
3722
3727
|
});
|
|
3723
|
-
for (const k of Object.keys(
|
|
3728
|
+
for (const k of Object.keys(field2.choiceColors ?? {})) if (!field2.choices.includes(k)) say(`${at}.choiceColors`, `It colours "${k}", which is not one of the field's choices.`);
|
|
3724
3729
|
const distinct = new Set(colors.filter(Boolean)).size;
|
|
3725
|
-
const need = Math.min(
|
|
3726
|
-
if (distinct < need) say(`${at}.choiceColors`, `${
|
|
3730
|
+
const need = Math.min(field2.choices.length, CHOICE_COLORS_DISTINCT_UP_TO);
|
|
3731
|
+
if (distinct < need) say(`${at}.choiceColors`, `${field2.choices.length} choices wear only ${distinct} distinct colour(s); each choice needs its own (${need}), so the board reads at a glance.`);
|
|
3727
3732
|
}
|
|
3728
|
-
if (isNumber(
|
|
3729
|
-
const kinds2 = new Set((
|
|
3730
|
-
if (!kinds2.has("min") || !kinds2.has("max")) say(`${at}.rules`, `"${
|
|
3731
|
-
const seeds = table.rows.map((r) => parseNumeric(storeText(r.values?.[
|
|
3733
|
+
if (isNumber(field2) && !isComputed(field2)) {
|
|
3734
|
+
const kinds2 = new Set((field2.rules ?? []).map((r) => r.kind));
|
|
3735
|
+
if (!kinds2.has("min") || !kinds2.has("max")) say(`${at}.rules`, `"${field2.label}" is measured, so it declares the least and the most a real value can be (min and max rules).`);
|
|
3736
|
+
const seeds = table.rows.map((r) => parseNumeric(storeText(r.values?.[field2.key]) ?? "")).filter((v) => !!v).map((v) => Number(v.n) / Number(v.d));
|
|
3732
3737
|
if (seeds.length) {
|
|
3733
3738
|
const hi = Math.max(...seeds);
|
|
3734
3739
|
const lo = Math.min(...seeds);
|
|
3735
|
-
if (new Set(seeds).size < 2) say(`${at}`, `Every seed row holds the same ${
|
|
3736
|
-
for (const rule of
|
|
3740
|
+
if (new Set(seeds).size < 2) say(`${at}`, `Every seed row holds the same ${field2.label}; real records vary.`);
|
|
3741
|
+
for (const rule of field2.rules ?? []) {
|
|
3737
3742
|
const bound = Number(rule.value);
|
|
3738
|
-
if (rule.kind === "max" && hi > 0 && bound > 100 * hi) say(`${at}.rules`, `The most "${
|
|
3739
|
-
if (rule.kind === "min" && bound < -1e3 * Math.abs(lo)) say(`${at}.rules`, `The least "${
|
|
3740
|
-
if (rule.kind === "min" && bound < 0 && lo >= 0) say(`${at}.rules`, `"${
|
|
3743
|
+
if (rule.kind === "max" && hi > 0 && bound > 100 * hi) say(`${at}.rules`, `The most "${field2.label}" allows (${rule.value}) is over 100\xD7 the largest real value (${hi}); write the real ceiling.`);
|
|
3744
|
+
if (rule.kind === "min" && bound < -1e3 * Math.abs(lo)) say(`${at}.rules`, `The least "${field2.label}" allows (${rule.value}) is far below any real value (${lo}); write the real floor.`);
|
|
3745
|
+
if (rule.kind === "min" && bound < 0 && lo >= 0) say(`${at}.rules`, `"${field2.label}" allows negative values, and no real one is negative; the floor is 0 or above.`);
|
|
3741
3746
|
}
|
|
3742
3747
|
}
|
|
3743
3748
|
}
|
|
3744
|
-
if (
|
|
3745
|
-
const via =
|
|
3749
|
+
if (field2.parityType === "lookup" || field2.parityType === "rollup") {
|
|
3750
|
+
const via = field2.via ? fieldOf(table.token, field2.via) : void 0;
|
|
3746
3751
|
if (!via || !isRelation(via) || !via.relationTarget) {
|
|
3747
|
-
say(`${at}.via`, `A ${
|
|
3752
|
+
say(`${at}.via`, `A ${field2.parityType} reads THROUGH a relation field of this table; "${field2.via ?? "(none)"}" is not one.`);
|
|
3748
3753
|
} else {
|
|
3749
3754
|
hasLookupOrRollup = true;
|
|
3750
|
-
if (!
|
|
3751
|
-
if (
|
|
3752
|
-
if (
|
|
3755
|
+
if (!field2.pick || !fieldOf(via.relationTarget, field2.pick)) say(`${at}.pick`, `It reads "${field2.pick ?? "(nothing)"}" on "${via.relationTarget}", which that table does not declare.`);
|
|
3756
|
+
if (field2.parityType === "rollup" && !field2.agg) say(`${at}.agg`, "A rollup says how it adds up the far side.");
|
|
3757
|
+
if (field2.parityType === "rollup" && !via.multi && (via.relationMax ?? 1) <= 1) say(`${at}.via`, `"${field2.label}" adds up "${via.label}", which links one record. Make "${via.label}" a list (multi), or read its one value with a lookup.`);
|
|
3753
3758
|
}
|
|
3754
3759
|
}
|
|
3755
|
-
if (
|
|
3756
|
-
if (isRelation(
|
|
3760
|
+
if (field2.parityType === "entity_reference" && !field2.allowedTypes?.length) say(`${at}.allowedTypes`, "A column that points at the platform names the kinds it may point at.");
|
|
3761
|
+
if (isRelation(field2) && field2.relationTarget && !byToken.has(field2.relationTarget)) say(`${at}.relationTarget`, `It points at "${field2.relationTarget}", which is neither a table nor a shared block of this template.`);
|
|
3757
3762
|
});
|
|
3758
3763
|
const seen = /* @__PURE__ */ new Map();
|
|
3759
3764
|
const titles = /* @__PURE__ */ new Set();
|
|
@@ -3763,47 +3768,47 @@ function validateTemplate(raw, options = {}) {
|
|
|
3763
3768
|
const twin = seen.get(sig);
|
|
3764
3769
|
if (twin) say(rat, `This row is identical to "${twin}" apart from its key. Every seed row is a different real record.`);
|
|
3765
3770
|
seen.set(sig, row.key);
|
|
3766
|
-
for (const
|
|
3767
|
-
if (!isRelation(
|
|
3768
|
-
const target = byToken.get(
|
|
3769
|
-
const v = row.values?.[
|
|
3771
|
+
for (const field2 of table.fields) {
|
|
3772
|
+
if (!isRelation(field2) || !field2.relationTarget || field2.parityType === "attachment") continue;
|
|
3773
|
+
const target = byToken.get(field2.relationTarget);
|
|
3774
|
+
const v = row.values?.[field2.key];
|
|
3770
3775
|
if (!target || v === void 0 || v === null) continue;
|
|
3771
3776
|
for (const ref2 of Array.isArray(v) ? v : [v]) {
|
|
3772
|
-
if (typeof ref2 === "string" && !target.rows.some((r) => r.key === ref2)) say(`${rat}.values.${
|
|
3777
|
+
if (typeof ref2 === "string" && !target.rows.some((r) => r.key === ref2)) say(`${rat}.values.${field2.key}`, `It points at the row "${ref2}", which does not exist in "${field2.relationTarget}".`);
|
|
3773
3778
|
}
|
|
3774
3779
|
}
|
|
3775
3780
|
const title = storeText(row.values?.[table.titleField]);
|
|
3776
3781
|
if (title) titles.add(title.trim().toLowerCase());
|
|
3777
|
-
for (const
|
|
3778
|
-
const v = row.values?.[
|
|
3782
|
+
for (const field2 of table.fields) {
|
|
3783
|
+
const v = row.values?.[field2.key];
|
|
3779
3784
|
if (v === void 0 || v === null || v === "") continue;
|
|
3780
|
-
for (const rule of
|
|
3785
|
+
for (const rule of field2.rules ?? []) {
|
|
3781
3786
|
const t = storeText(v) ?? "";
|
|
3782
3787
|
if (rule.kind === "min" || rule.kind === "max") {
|
|
3783
3788
|
const n = parseNumeric(t);
|
|
3784
3789
|
const bound = parseNumeric(String(rule.value));
|
|
3785
3790
|
if (n && bound && (rule.kind === "min" ? qCmp(n, bound) < 0 : qCmp(n, bound) > 0)) {
|
|
3786
|
-
say(`${rat}.values.${
|
|
3791
|
+
say(`${rat}.values.${field2.key}`, `${t} breaks its own rule: "${field2.label}" is at ${rule.kind === "min" ? "least" : "most"} ${rule.value}.`);
|
|
3787
3792
|
}
|
|
3788
3793
|
} else if (rule.kind === "length") {
|
|
3789
|
-
if (rule.value !== void 0 && t.length > Number(rule.value)) say(`${rat}.values.${
|
|
3790
|
-
if (rule.min !== void 0 && t.length < rule.min) say(`${rat}.values.${
|
|
3794
|
+
if (rule.value !== void 0 && t.length > Number(rule.value)) say(`${rat}.values.${field2.key}`, `"${t}" is longer than "${field2.label}" allows (${rule.value}).`);
|
|
3795
|
+
if (rule.min !== void 0 && t.length < rule.min) say(`${rat}.values.${field2.key}`, `"${t}" is shorter than "${field2.label}" allows (${rule.min}).`);
|
|
3791
3796
|
} else if (rule.kind === "pattern" && rule.value !== void 0) {
|
|
3792
3797
|
let re = null;
|
|
3793
3798
|
try {
|
|
3794
3799
|
re = new RegExp(String(rule.value));
|
|
3795
3800
|
} catch {
|
|
3796
|
-
say(`${tat}.fields (${
|
|
3801
|
+
say(`${tat}.fields (${field2.key}).rules`, `The pattern "${rule.value}" is not a pattern.`);
|
|
3797
3802
|
}
|
|
3798
|
-
if (re && !re.test(t)) say(`${rat}.values.${
|
|
3803
|
+
if (re && !re.test(t)) say(`${rat}.values.${field2.key}`, `"${t}" does not match "${field2.label}"'s own pattern.`);
|
|
3799
3804
|
}
|
|
3800
3805
|
}
|
|
3801
3806
|
}
|
|
3802
3807
|
});
|
|
3803
|
-
for (const
|
|
3804
|
-
if (!(
|
|
3805
|
-
const vals = table.rows.map((r) => storeText(r.values?.[
|
|
3806
|
-
if (new Set(vals).size !== vals.length) say(`${tat}.fields (${
|
|
3808
|
+
for (const field2 of table.fields) {
|
|
3809
|
+
if (!(field2.rules ?? []).some((r) => r.kind === "unique")) continue;
|
|
3810
|
+
const vals = table.rows.map((r) => storeText(r.values?.[field2.key])).filter((v) => !!v).map((v) => v.trim().toLowerCase());
|
|
3811
|
+
if (new Set(vals).size !== vals.length) say(`${tat}.fields (${field2.key})`, `"${field2.label}" is unique, and two seed rows share a value.`);
|
|
3807
3812
|
}
|
|
3808
3813
|
if (table.rows.length >= 2 && titles.size < Math.ceil(table.rows.length * TITLE_DISTINCT_SHARE)) {
|
|
3809
3814
|
say(`${tat}.rows`, `Only ${titles.size} of ${table.rows.length} rows have a title of their own; a list of look-alike names reads as generated.`);
|
|
@@ -5032,8 +5037,8 @@ function validateTemplate(raw, options = {}) {
|
|
|
5032
5037
|
}
|
|
5033
5038
|
const currencies = new Set(fields.filter((f) => f.parityType === "currency").map((f) => (f.unit ?? "").toUpperCase()));
|
|
5034
5039
|
if (currencies.size > 1) say("tables", `Money is kept in ${[...currencies].join(" and ")}; one business keeps its books in one currency.`);
|
|
5035
|
-
const GOOD =
|
|
5036
|
-
const BAD =
|
|
5040
|
+
const GOOD = GOOD_OUTCOME;
|
|
5041
|
+
const BAD = BAD_OUTCOME;
|
|
5037
5042
|
for (const t of own) {
|
|
5038
5043
|
for (const f of t.fields) {
|
|
5039
5044
|
for (const [choice, color] of Object.entries(f.choiceColors ?? {})) {
|
|
@@ -5210,14 +5215,25 @@ function validateTemplate(raw, options = {}) {
|
|
|
5210
5215
|
if (columns && !(status2 && /writes "booked" and "cancelled"/.test(columns))) say(`${at}.table`, columns);
|
|
5211
5216
|
for (const qq of e.questions ?? []) if (["slot", "status", "booked_with"].includes(qq.fieldKey)) say(`${at}.questions`, `A booking page never asks for "${qq.fieldKey}": the store fills it in from the slot that was held.`);
|
|
5212
5217
|
const a = e.availability ?? {};
|
|
5213
|
-
const
|
|
5214
|
-
|
|
5218
|
+
const wholeNumber = (key, what) => {
|
|
5219
|
+
const v = a[key];
|
|
5220
|
+
if (v === void 0 || typeof v === "number" && Number.isInteger(v)) return true;
|
|
5221
|
+
say(`${at}.availability.${key}`, key === "days" ? `${what} is ONE whole number such as 21 (how many days ahead a person may book); the weekdays belong in windows, not here.` : `${what} is one whole number; this says ${JSON.stringify(v)}.`);
|
|
5222
|
+
return false;
|
|
5223
|
+
};
|
|
5224
|
+
const rng = (v, lo, hi, what, key) => {
|
|
5225
|
+
if (v !== void 0 && wholeNumber(key, what) && (v < lo || v > hi)) say(`${at}.availability`, `${what} is between ${lo} and ${hi}; this says ${v}.`);
|
|
5215
5226
|
};
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5227
|
+
(a.windows ?? []).forEach((w, wi) => {
|
|
5228
|
+
const ww = w;
|
|
5229
|
+
if (!Number.isInteger(ww.weekday)) say(`${at}.availability.windows[${wi}]`, `A window's weekday is one whole number, 0 (Sunday) to 6 (Saturday); this says ${JSON.stringify(ww.weekday)}.`);
|
|
5230
|
+
for (const k of ["from", "to"]) if (typeof ww[k] !== "string" || !/^\d{2}:\d{2}$/.test(ww[k])) say(`${at}.availability.windows[${wi}]`, `A window's "${k}" is a time of day written "09:00"; this says ${JSON.stringify(ww[k])}.`);
|
|
5231
|
+
});
|
|
5232
|
+
rng(a.slotMinutes, 5, 480, "Slot length (minutes)", "slotMinutes");
|
|
5233
|
+
rng(a.bufferMinutes, 0, 480, "The buffer (minutes)", "bufferMinutes");
|
|
5234
|
+
rng(a.leadMinutes, 0, 43200, "Lead time (minutes)", "leadMinutes");
|
|
5235
|
+
rng(a.maxPerDay, 1, 100, "Appointments a day", "maxPerDay");
|
|
5236
|
+
rng(a.days, 1, 90, "Days ahead", "days");
|
|
5221
5237
|
if (a.timezone) {
|
|
5222
5238
|
try {
|
|
5223
5239
|
new Intl.DateTimeFormat("en-US", { timeZone: a.timezone });
|
|
@@ -5873,6 +5889,8 @@ var RESTRICTED_KEY = /(^|_)(member_id|ssn|social_security|dob|date_of_birth|birt
|
|
|
5873
5889
|
var RESTRICTED_LABEL = /\b(bank|routing number|iban|card on file|credit card|card number|cvv|medical conditions?|medications?|diagnos[ie]s|case notes|legal notes|privileged)\b/;
|
|
5874
5890
|
var PERSONAL_ONLY = /(^|_)(dob|date_of_birth|birth_date|allerg(y|ies)|diagnos(is|es)|medical_conditions?|conditions|medications?|current_medications|clinical_notes?|visit_notes?|progress_notes?|soap_notes?|member_id|ssn|social_security)($|_)/;
|
|
5875
5891
|
var holdsPeople = (t) => t.subject ? t.subject === "person" : t.fields.some((f) => /name/.test(f.key)) && t.fields.some((f) => f.parityType === "phone" || f.parityType === "email");
|
|
5892
|
+
var GOOD_OUTCOME = /^(completed?|done|paid|approved|won|closed won|booked|confirmed|delivered|resolved|published|signed|passed|hired|accepted)$/i;
|
|
5893
|
+
var BAD_OUTCOME = /^(cancell?ed|skipped|lost|closed lost|denied|rejected|no[- ]show|failed|overdue|declined|void(ed)?|refunded|churned|expired|blocked)$/i;
|
|
5876
5894
|
function sensitivityFloorOf(table, f) {
|
|
5877
5895
|
const text = `${f.key} ${norm(f.label)}`;
|
|
5878
5896
|
const billingTable = /(invoice|bill|statement|receipt|payment|charge|fee|order|account)/i.test(`${table.token} ${table.labelSingular ?? ""}`);
|
|
@@ -6350,7 +6368,719 @@ async function runTemplateDoor(source, door, organizationId, id, options = {}) {
|
|
|
6350
6368
|
function planTables(plan) {
|
|
6351
6369
|
return plan.steps.flatMap((s) => (s.made ?? []).filter((m) => m.kind === "table").map((m) => m.ref));
|
|
6352
6370
|
}
|
|
6371
|
+
|
|
6372
|
+
// src/templates/describe/shape.ts
|
|
6373
|
+
var GALLERY_ONLY = ["catalogueId", "strengths", "provenance", "walk", "agent", "requires", "persona", "extraAgents", "workflows", "guide", "__kind"];
|
|
6374
|
+
function describeSpec(template) {
|
|
6375
|
+
const s = { ...template };
|
|
6376
|
+
for (const k of GALLERY_ONLY) delete s[k];
|
|
6377
|
+
s.specVersion = 1;
|
|
6378
|
+
if (typeof s.version !== "number") s.version = 1;
|
|
6379
|
+
if (typeof s.audience !== "string") s.audience = "organization";
|
|
6380
|
+
if (typeof s.cleanupTag !== "string" && typeof s.id === "string") s.cleanupTag = `describe:${s.id}`;
|
|
6381
|
+
for (const k of ["relationships", "sharedBlocks", "views", "forms", "dimensions", "extras"]) if (!Array.isArray(s[k])) s[k] = [];
|
|
6382
|
+
if (Array.isArray(s.tables)) s.tables = s.tables.map(describeTable);
|
|
6383
|
+
return s;
|
|
6384
|
+
}
|
|
6385
|
+
var TABLE_DEFAULTS = { type: "entity", display: "grid", weight: "light", ordered: false };
|
|
6386
|
+
var CONTACT_KINDS = /* @__PURE__ */ new Set(["phone", "email"]);
|
|
6387
|
+
function describeTable(t) {
|
|
6388
|
+
const own = Array.isArray(t.fields) ? t.fields : null;
|
|
6389
|
+
const shape = { token: String(t.token ?? ""), labelSingular: String(t.labelSingular ?? ""), subject: t.subject, fields: own ?? [] };
|
|
6390
|
+
const fields = own ? own.map((f) => describeField(f, shape)) : t.fields;
|
|
6391
|
+
return {
|
|
6392
|
+
...TABLE_DEFAULTS,
|
|
6393
|
+
...t,
|
|
6394
|
+
...typeof t.labelPlural !== "string" && typeof t.name === "string" ? { labelPlural: t.name } : {},
|
|
6395
|
+
fields,
|
|
6396
|
+
rows: Array.isArray(t.rows) ? t.rows : []
|
|
6397
|
+
};
|
|
6398
|
+
}
|
|
6399
|
+
function describeField(f, table) {
|
|
6400
|
+
const contact = CONTACT_KINDS.has(String(f.parityType ?? ""));
|
|
6401
|
+
const floor = sensitivityFloorOf(table, { key: String(f.key ?? ""), label: String(f.label ?? ""), parityType: String(f.parityType ?? "") });
|
|
6402
|
+
return {
|
|
6403
|
+
...contact ? { format: f.parityType } : {},
|
|
6404
|
+
sensitivity: floor ?? "internal",
|
|
6405
|
+
contextPolicy: floor ? "exclude" : "include",
|
|
6406
|
+
...f
|
|
6407
|
+
};
|
|
6408
|
+
}
|
|
6409
|
+
|
|
6410
|
+
// src/templates/describe/merge.ts
|
|
6411
|
+
var clone = (v) => v === void 0 ? v : JSON.parse(JSON.stringify(v));
|
|
6412
|
+
var labelOfKey = (key) => {
|
|
6413
|
+
const w = key.replace(/_/g, " ").trim();
|
|
6414
|
+
return w ? w[0].toUpperCase() + w.slice(1) : key;
|
|
6415
|
+
};
|
|
6416
|
+
var field = (t, key) => t.fields.find((f) => f.key === key);
|
|
6417
|
+
function skeletonTable(p) {
|
|
6418
|
+
return {
|
|
6419
|
+
token: p.token,
|
|
6420
|
+
name: p.name,
|
|
6421
|
+
labelSingular: p.labelSingular,
|
|
6422
|
+
labelPlural: p.name,
|
|
6423
|
+
subject: p.subject,
|
|
6424
|
+
titleField: p.titleField.key,
|
|
6425
|
+
describes: p.purpose.slice(0, 140),
|
|
6426
|
+
fields: [],
|
|
6427
|
+
rows: []
|
|
6428
|
+
};
|
|
6429
|
+
}
|
|
6430
|
+
function ensureSkeleton(t, p) {
|
|
6431
|
+
if (typeof t.name !== "string") t.name = p.name;
|
|
6432
|
+
if (typeof t.labelSingular !== "string") t.labelSingular = p.labelSingular;
|
|
6433
|
+
if (typeof t.subject !== "string") t.subject = p.subject;
|
|
6434
|
+
if (typeof t.describes !== "string" || !t.describes.trim()) t.describes = p.purpose.slice(0, 140);
|
|
6435
|
+
if (!Array.isArray(t.fields)) t.fields = [];
|
|
6436
|
+
if (typeof t.titleField !== "string" || !t.titleField) t.titleField = p.titleField.key;
|
|
6437
|
+
if (!field(t, p.titleField.key)) t.fields.unshift({ key: p.titleField.key, label: p.titleField.label, parityType: "text", required: true });
|
|
6438
|
+
if (p.status && !field(t, "status")) {
|
|
6439
|
+
t.fields.push({ key: "status", label: "Status", parityType: "select", required: true, choices: [...p.status.choices], default: p.status.default });
|
|
6440
|
+
}
|
|
6441
|
+
if (p.slot && !field(t, "slot")) t.fields.push({ key: "slot", label: "Visit time", parityType: "datetime", format: "datetime", required: true });
|
|
6442
|
+
for (const l of p.links) {
|
|
6443
|
+
if (field(t, l.key)) continue;
|
|
6444
|
+
t.fields.push({ key: l.key, label: l.label, parityType: "relation", relationTarget: l.target, ...l.many ? { multi: true } : { relationMax: 1 } });
|
|
6445
|
+
}
|
|
6446
|
+
}
|
|
6447
|
+
function addExistingKeys(t, p, existing) {
|
|
6448
|
+
if (!p.reuse) return;
|
|
6449
|
+
const e = existing.find((x) => x.id === p.reuse.existing_table_id);
|
|
6450
|
+
if (!e) return;
|
|
6451
|
+
for (const key of p.reuse.keepKeys) {
|
|
6452
|
+
if (field(t, key)) continue;
|
|
6453
|
+
const ef = e.fields.find((f) => f.key === key);
|
|
6454
|
+
if (!ef) continue;
|
|
6455
|
+
t.fields.push({ key, label: ef.label, parityType: ef.kind, ...ef.choices?.length ? { choices: [...ef.choices] } : {} });
|
|
6456
|
+
}
|
|
6457
|
+
}
|
|
6458
|
+
function addRequest(t, r) {
|
|
6459
|
+
if (field(t, r.key)) return;
|
|
6460
|
+
const f = { key: r.key, label: r.label, parityType: r.parityType };
|
|
6461
|
+
if (r.choices?.length) f.choices = [...r.choices];
|
|
6462
|
+
if (r.askOfSubject) f.askOfSubject = true;
|
|
6463
|
+
if (r.askOfGuardian) f.askOfGuardian = true;
|
|
6464
|
+
if (r.required) f.required = true;
|
|
6465
|
+
if (r.help) f.help = r.help;
|
|
6466
|
+
t.fields.push(f);
|
|
6467
|
+
}
|
|
6468
|
+
var CHOICE = ["select", "multi_select", "list"];
|
|
6469
|
+
var STAGE_KEY = /(^|_)(status|stage)$/;
|
|
6470
|
+
function viewHolds(v, tables) {
|
|
6471
|
+
const t = tables.get(v.table);
|
|
6472
|
+
if (!t) return false;
|
|
6473
|
+
const has = (k) => typeof k === "string" && !!field(t, k);
|
|
6474
|
+
const x = v;
|
|
6475
|
+
const kindOf = (k) => t.fields.find((f) => f.key === k)?.parityType;
|
|
6476
|
+
if (v.kind === "kanban" && !(has(x.groupBy) && CHOICE.includes(kindOf(x.groupBy)))) return false;
|
|
6477
|
+
if (v.kind === "calendar" && !(has(x.dateField) && kindOf(x.dateField) === "datetime")) return false;
|
|
6478
|
+
if (v.kind === "timeline" && !(has(x.startField) && has(x.endField))) return false;
|
|
6479
|
+
if (v.kind === "gallery" && !has(x.coverField)) return false;
|
|
6480
|
+
if (x.groupBy !== void 0 && !has(x.groupBy)) return false;
|
|
6481
|
+
for (const k of Object.keys(x.filters ?? {})) if (!has(k)) return false;
|
|
6482
|
+
for (const k of Object.keys(x.anyOf ?? {})) if (!has(k)) return false;
|
|
6483
|
+
for (const s of x.sorts ?? []) if (!has(s.field)) return false;
|
|
6484
|
+
return typeof v.token === "string" && !!v.token && typeof v.name === "string" && !!v.name;
|
|
6485
|
+
}
|
|
6486
|
+
function fallbackView(t) {
|
|
6487
|
+
const plural = typeof t.name === "string" ? t.name : labelOfKey(t.token);
|
|
6488
|
+
const stage = t.fields.find((f) => STAGE_KEY.test(f.key) && CHOICE.includes(f.parityType));
|
|
6489
|
+
if (stage) return { token: `${t.token}_board`, name: `${plural} board`, table: t.token, kind: "kanban", groupBy: stage.key, isDefault: true };
|
|
6490
|
+
if (t.fields.some((f) => f.key === "slot" && f.parityType === "datetime")) {
|
|
6491
|
+
return { token: `${t.token}_calendar`, name: `${plural} calendar`, table: t.token, kind: "calendar", dateField: "slot", isDefault: true };
|
|
6492
|
+
}
|
|
6493
|
+
return { token: `${t.token}_list`, name: `All ${plural.toLowerCase()}`, table: t.token, kind: "grid", isDefault: true, sorts: [{ field: String(t.titleField), direction: "asc" }] };
|
|
6494
|
+
}
|
|
6495
|
+
function mergeViews(plan, part, tables) {
|
|
6496
|
+
const given = Array.isArray(part?.views) ? part.views : [];
|
|
6497
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6498
|
+
const kept = [];
|
|
6499
|
+
for (const v of given) {
|
|
6500
|
+
if (!v || !viewHolds(v, tables) || seen.has(v.token)) continue;
|
|
6501
|
+
seen.add(v.token);
|
|
6502
|
+
kept.push(clone(v));
|
|
6503
|
+
}
|
|
6504
|
+
const defaulted = /* @__PURE__ */ new Set();
|
|
6505
|
+
for (const v of kept) {
|
|
6506
|
+
if (!v.isDefault) continue;
|
|
6507
|
+
if (defaulted.has(v.table)) delete v.isDefault;
|
|
6508
|
+
else defaulted.add(v.table);
|
|
6509
|
+
}
|
|
6510
|
+
if (kept.length) return kept;
|
|
6511
|
+
const wanted = [...new Set(plan.views.map((v) => v.table))].filter((tok) => tables.has(tok));
|
|
6512
|
+
const targets = wanted.length ? wanted : [...tables.keys()].slice(0, 1);
|
|
6513
|
+
return targets.map((tok) => fallbackView(tables.get(tok)));
|
|
6514
|
+
}
|
|
6515
|
+
function sharedBlocksFor(tables) {
|
|
6516
|
+
const own = new Set(tables.map((t) => t.token));
|
|
6517
|
+
const out = [];
|
|
6518
|
+
for (const id of SHARED_BLOCK_IDS) {
|
|
6519
|
+
const token = SHARED_BLOCKS[id].table;
|
|
6520
|
+
if (own.has(token)) continue;
|
|
6521
|
+
if (tables.some((t) => t.fields.some((f) => f.parityType === "relation" && f.relationTarget === token))) out.push({ block: id, rows: [] });
|
|
6522
|
+
}
|
|
6523
|
+
return out;
|
|
6524
|
+
}
|
|
6525
|
+
function reusesOf(plan) {
|
|
6526
|
+
return plan.tables.filter((t) => t.reuse).map((t) => ({ token: t.token, existing_table_id: t.reuse.existing_table_id }));
|
|
6527
|
+
}
|
|
6528
|
+
function mergeDescribeParts(plan, tables, doors, views, existingTables = []) {
|
|
6529
|
+
const out = clone(Array.isArray(tables?.tables) ? tables.tables : []);
|
|
6530
|
+
const byToken = new Map(out.map((t) => [t.token, t]));
|
|
6531
|
+
for (const p of plan.tables) {
|
|
6532
|
+
let t = byToken.get(p.token);
|
|
6533
|
+
if (!t) {
|
|
6534
|
+
t = skeletonTable(p);
|
|
6535
|
+
out.push(t);
|
|
6536
|
+
byToken.set(p.token, t);
|
|
6537
|
+
}
|
|
6538
|
+
ensureSkeleton(t, p);
|
|
6539
|
+
addExistingKeys(t, p, existingTables);
|
|
6540
|
+
}
|
|
6541
|
+
for (const r of doors?.fieldRequests ?? []) {
|
|
6542
|
+
const t = byToken.get(r.table);
|
|
6543
|
+
if (t) addRequest(t, r);
|
|
6544
|
+
}
|
|
6545
|
+
const spec = {
|
|
6546
|
+
specVersion: 1,
|
|
6547
|
+
version: 1,
|
|
6548
|
+
id: plan.id,
|
|
6549
|
+
useCase: plan.useCase,
|
|
6550
|
+
industry: plan.industry,
|
|
6551
|
+
vertical: plan.vertical,
|
|
6552
|
+
job: plan.job,
|
|
6553
|
+
teaches: plan.teaches,
|
|
6554
|
+
audience: plan.audience,
|
|
6555
|
+
cleanupTag: `describe:${plan.id}`,
|
|
6556
|
+
business: clone(plan.business),
|
|
6557
|
+
tables: out,
|
|
6558
|
+
sharedBlocks: sharedBlocksFor(out),
|
|
6559
|
+
relationships: clone(tables?.relationships ?? []),
|
|
6560
|
+
views: mergeViews(plan, views, byToken),
|
|
6561
|
+
forms: clone(doors?.forms ?? []),
|
|
6562
|
+
dimensions: [],
|
|
6563
|
+
extras: doors?.booking ? [clone(doors.booking)] : []
|
|
6564
|
+
};
|
|
6565
|
+
return spec;
|
|
6566
|
+
}
|
|
6567
|
+
|
|
6568
|
+
// src/templates/describe/autofix.ts
|
|
6569
|
+
var clone2 = (v) => JSON.parse(JSON.stringify(v));
|
|
6570
|
+
var SENSITIVITY_RANK2 = ["public", "internal", "confidential", "restricted"];
|
|
6571
|
+
var isChoice2 = (f) => ["select", "multi_select", "list"].includes(f.parityType);
|
|
6572
|
+
var isComputed2 = (f) => ["formula", "lookup", "rollup", "autonumber", "created_time", "modified_time"].includes(f.parityType) || !!f.source && f.source !== "manual";
|
|
6573
|
+
var MINOR_VERTICAL = /pediatric|paediatric|youth|child|kids?\b|school|daycare|day care|junior|teen|infant|nursery|camp\b/i;
|
|
6574
|
+
var NEUTRAL_ORDER = ["amber", "blue", "violet", "teal", "slate", "orange", "yellow", "pink", "brown", "gray", "green", "red"];
|
|
6575
|
+
var GOOD_ORDER = ["green", "teal", "blue", "violet", "slate", "amber", "orange", "yellow", "pink", "brown", "gray"];
|
|
6576
|
+
var BAD_ORDER = ["red", "orange", "gray", "slate", "brown", "pink", "amber", "yellow", "violet", "blue", "teal"];
|
|
6577
|
+
var DISTINCT_UP_TO = 7;
|
|
6578
|
+
var lower = (s) => s.trim().toLowerCase();
|
|
6579
|
+
function raiseSensitivity(spec, fixes) {
|
|
6580
|
+
for (const t of spec.tables) {
|
|
6581
|
+
for (const f of t.fields) {
|
|
6582
|
+
const at = `tables.${t.token}.fields.${f.key}`;
|
|
6583
|
+
const floor = sensitivityFloorOf(t, f);
|
|
6584
|
+
const rank = (s) => SENSITIVITY_RANK2.indexOf(s);
|
|
6585
|
+
if (floor && rank(f.sensitivity) < rank(floor)) {
|
|
6586
|
+
fixes.push(`${at}: sensitivity ${f.sensitivity} raised to ${floor}`);
|
|
6587
|
+
f.sensitivity = floor;
|
|
6588
|
+
}
|
|
6589
|
+
if (floor === "restricted" && f.contextPolicy !== "exclude") {
|
|
6590
|
+
fixes.push(`${at}: contextPolicy ${f.contextPolicy} set to exclude (restricted)`);
|
|
6591
|
+
f.contextPolicy = "exclude";
|
|
6592
|
+
}
|
|
6593
|
+
if (/note/.test(`${f.key} ${lower(f.label)}`) && f.sensitivity === "public") {
|
|
6594
|
+
fixes.push(`${at}: notes are never public; raised to internal`);
|
|
6595
|
+
f.sensitivity = "internal";
|
|
6596
|
+
}
|
|
6597
|
+
}
|
|
6598
|
+
}
|
|
6599
|
+
}
|
|
6600
|
+
function flagAskedOfSubject(spec, ctx, fixes) {
|
|
6601
|
+
const minor = MINOR_VERTICAL.test(`${spec.vertical ?? ""} ${ctx.plan?.vertical ?? ""}`);
|
|
6602
|
+
const byToken = new Map(spec.tables.map((t) => [t.token, t]));
|
|
6603
|
+
for (const form of spec.forms) {
|
|
6604
|
+
if (form.audience !== "public") continue;
|
|
6605
|
+
const t = byToken.get(form.table);
|
|
6606
|
+
if (!t) continue;
|
|
6607
|
+
for (const q2 of form.fields ?? []) {
|
|
6608
|
+
const f = t.fields.find((x) => x.key === q2.fieldKey);
|
|
6609
|
+
if (!f) continue;
|
|
6610
|
+
if (f.sensitivity !== "confidential" && f.sensitivity !== "restricted") continue;
|
|
6611
|
+
if (f.askOfSubject || f.askOfGuardian) continue;
|
|
6612
|
+
if (minor) f.askOfGuardian = true;
|
|
6613
|
+
else f.askOfSubject = true;
|
|
6614
|
+
fixes.push(`tables.${t.token}.fields.${f.key}: ${minor ? "askOfGuardian" : "askOfSubject"} set (asked on the public form "${form.token}")`);
|
|
6615
|
+
}
|
|
6616
|
+
}
|
|
6617
|
+
}
|
|
6618
|
+
var DOOR_KEYS = ["slot", "status", "booked_with"];
|
|
6619
|
+
function colorOf(name) {
|
|
6620
|
+
return /^booked$/i.test(name) ? "blue" : "slate";
|
|
6621
|
+
}
|
|
6622
|
+
function fixBooking(spec, fixes) {
|
|
6623
|
+
for (const e of spec.extras) {
|
|
6624
|
+
if (e.kind !== "booking") continue;
|
|
6625
|
+
const b = e;
|
|
6626
|
+
const t = spec.tables.find((x) => x.token === b.table);
|
|
6627
|
+
if (t) {
|
|
6628
|
+
const at = `tables.${t.token}.fields`;
|
|
6629
|
+
const slot = t.fields.find((f) => f.key === "slot");
|
|
6630
|
+
if (!slot) {
|
|
6631
|
+
t.fields.push({ key: "slot", label: "Visit time", parityType: "datetime", format: "datetime", required: true, sensitivity: "internal", contextPolicy: "include" });
|
|
6632
|
+
fixes.push(`${at}.slot: added the required datetime the booking page writes`);
|
|
6633
|
+
} else {
|
|
6634
|
+
if (slot.parityType !== "datetime") {
|
|
6635
|
+
slot.parityType = "datetime";
|
|
6636
|
+
fixes.push(`${at}.slot: made a datetime`);
|
|
6637
|
+
}
|
|
6638
|
+
if (!slot.required) {
|
|
6639
|
+
slot.required = true;
|
|
6640
|
+
fixes.push(`${at}.slot: made required`);
|
|
6641
|
+
}
|
|
6642
|
+
if (slot.format !== "datetime") slot.format = "datetime";
|
|
6643
|
+
}
|
|
6644
|
+
let status = t.fields.find((f) => f.key === "status");
|
|
6645
|
+
if (!status) {
|
|
6646
|
+
status = { key: "status", label: "Status", parityType: "select", required: true, choices: ["Booked", "Cancelled"], default: "Booked", choiceColors: { Booked: "blue", Cancelled: "slate" }, sensitivity: "internal", contextPolicy: "include" };
|
|
6647
|
+
t.fields.push(status);
|
|
6648
|
+
fixes.push(`${at}.status: added a Booked / Cancelled choice field`);
|
|
6649
|
+
} else {
|
|
6650
|
+
if (!isChoice2(status)) {
|
|
6651
|
+
status.parityType = "select";
|
|
6652
|
+
status.choices = status.choices ?? [];
|
|
6653
|
+
fixes.push(`${at}.status: made a choice field`);
|
|
6654
|
+
}
|
|
6655
|
+
const have = new Set((status.choices ?? []).map(lower));
|
|
6656
|
+
for (const w of ["Booked", "Cancelled"]) {
|
|
6657
|
+
if (have.has(lower(w))) continue;
|
|
6658
|
+
status.choices = [...status.choices ?? [], w];
|
|
6659
|
+
status.choiceColors = { ...status.choiceColors ?? {}, [w]: colorOf(w) };
|
|
6660
|
+
fixes.push(`${at}.status: added the choice "${w}" the booking page writes`);
|
|
6661
|
+
}
|
|
6662
|
+
}
|
|
6663
|
+
}
|
|
6664
|
+
const before = b.questions?.length ?? 0;
|
|
6665
|
+
b.questions = (b.questions ?? []).filter((q2) => !DOOR_KEYS.includes(q2.fieldKey));
|
|
6666
|
+
if (b.questions.length !== before) fixes.push(`extras.${b.token}.questions: dropped ${before - b.questions.length} question(s) on slot/status/booked_with`);
|
|
6667
|
+
}
|
|
6668
|
+
}
|
|
6669
|
+
var DEFAULT_DAYS_AHEAD = 28;
|
|
6670
|
+
var AVAILABILITY_NUMBERS = ["slotMinutes", "bufferMinutes", "leadMinutes", "maxPerDay"];
|
|
6671
|
+
function fixAvailability(spec, fixes) {
|
|
6672
|
+
for (const e of spec.extras) {
|
|
6673
|
+
if (e.kind !== "booking") continue;
|
|
6674
|
+
const b = e;
|
|
6675
|
+
const a = b.availability;
|
|
6676
|
+
if (!a) continue;
|
|
6677
|
+
const at = `extras.${b.token}.availability`;
|
|
6678
|
+
if (Array.isArray(a.days)) {
|
|
6679
|
+
const weekdays = [...new Set(a.days.filter((d) => Number.isInteger(d) && d >= 0 && d <= 6))];
|
|
6680
|
+
a.days = DEFAULT_DAYS_AHEAD;
|
|
6681
|
+
fixes.push(`${at}.days: a weekday list is not "days ahead"; set to ${DEFAULT_DAYS_AHEAD} (weekdays belong in windows)`);
|
|
6682
|
+
const windows = Array.isArray(a.windows) ? a.windows : [];
|
|
6683
|
+
const model = windows[0];
|
|
6684
|
+
if (model && /^\d{2}:\d{2}$/.test(String(model.from))) {
|
|
6685
|
+
for (const d of weekdays.sort((x, y) => x - y)) {
|
|
6686
|
+
if (windows.some((w) => w.weekday === d)) continue;
|
|
6687
|
+
windows.push({ weekday: d, from: model.from, to: model.to });
|
|
6688
|
+
fixes.push(`${at}.windows: added a ${model.from}\u2013${model.to} window on weekday ${d} (it was listed in days)`);
|
|
6689
|
+
}
|
|
6690
|
+
a.windows = windows;
|
|
6691
|
+
}
|
|
6692
|
+
} else if (a.days !== void 0) {
|
|
6693
|
+
const n = typeof a.days === "string" && a.days.trim() !== "" ? Number(a.days) : a.days;
|
|
6694
|
+
if (typeof n === "number" && Number.isFinite(n) && n !== a.days) {
|
|
6695
|
+
a.days = Math.round(n);
|
|
6696
|
+
fixes.push(`${at}.days: written as one whole number (${a.days})`);
|
|
6697
|
+
} else if (typeof n === "number" && Number.isFinite(n) && !Number.isInteger(n)) {
|
|
6698
|
+
a.days = Math.round(n);
|
|
6699
|
+
fixes.push(`${at}.days: rounded to ${a.days}`);
|
|
6700
|
+
}
|
|
6701
|
+
}
|
|
6702
|
+
for (const k of AVAILABILITY_NUMBERS) {
|
|
6703
|
+
const v = a[k];
|
|
6704
|
+
if (v === void 0) continue;
|
|
6705
|
+
const n = typeof v === "string" && v.trim() !== "" ? Number(v) : v;
|
|
6706
|
+
if (typeof n === "number" && Number.isFinite(n) && (!Number.isInteger(n) || n !== v)) {
|
|
6707
|
+
a[k] = Math.round(n);
|
|
6708
|
+
fixes.push(`${at}.${k}: written as one whole number (${a[k]})`);
|
|
6709
|
+
}
|
|
6710
|
+
}
|
|
6711
|
+
for (const w of Array.isArray(a.windows) ? a.windows : []) {
|
|
6712
|
+
if (typeof w.weekday === "string" && /^\d$/.test(w.weekday)) {
|
|
6713
|
+
w.weekday = Number(w.weekday);
|
|
6714
|
+
fixes.push(`${at}.windows: a weekday written as a whole number`);
|
|
6715
|
+
}
|
|
6716
|
+
}
|
|
6717
|
+
}
|
|
6718
|
+
}
|
|
6719
|
+
function fixViewTokens(spec, fixes) {
|
|
6720
|
+
const taken = /* @__PURE__ */ new Set();
|
|
6721
|
+
const claim = (t) => typeof t === "string" && t.trim() !== "" && taken.add(t);
|
|
6722
|
+
for (const v of spec.views) claim(v.token);
|
|
6723
|
+
for (const f of spec.forms) claim(f.token);
|
|
6724
|
+
for (const e of spec.extras) claim(e.token);
|
|
6725
|
+
const snake = (x) => x.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "view";
|
|
6726
|
+
spec.views.forEach((v, i) => {
|
|
6727
|
+
if (typeof v.token === "string" && v.token.trim() !== "") return;
|
|
6728
|
+
const base = `${snake(String(v.table ?? "table"))}_${snake(String(v.kind ?? "view"))}`;
|
|
6729
|
+
let token = base;
|
|
6730
|
+
for (let n = 2; taken.has(token); n++) token = `${base}_${n}`;
|
|
6731
|
+
taken.add(token);
|
|
6732
|
+
v.token = token;
|
|
6733
|
+
fixes.push(`views[${i}]: had no token; named "${token}" (table and kind)`);
|
|
6734
|
+
});
|
|
6735
|
+
}
|
|
6736
|
+
function fixColours(spec, fixes) {
|
|
6737
|
+
for (const t of spec.tables) {
|
|
6738
|
+
for (const f of t.fields) {
|
|
6739
|
+
if (!isChoice2(f) || !f.choices?.length) continue;
|
|
6740
|
+
const choices = f.choices;
|
|
6741
|
+
const palette = CHOICE_COLORS;
|
|
6742
|
+
const colors = {};
|
|
6743
|
+
for (const [k, v] of Object.entries(f.choiceColors ?? {})) if (choices.includes(k) && palette.includes(v)) colors[k] = v;
|
|
6744
|
+
const orderOf = (c) => GOOD_OUTCOME.test(c) ? GOOD_ORDER : BAD_OUTCOME.test(c) ? BAD_ORDER : NEUTRAL_ORDER;
|
|
6745
|
+
const holder = (color, except) => choices.find((c) => c !== except && colors[c] === color);
|
|
6746
|
+
const unused = (order) => order.find((c) => !choices.some((x) => colors[x] === c));
|
|
6747
|
+
const need = Math.min(choices.length, DISTINCT_UP_TO);
|
|
6748
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6749
|
+
for (const c of choices) {
|
|
6750
|
+
const col = colors[c];
|
|
6751
|
+
if (col && seen.has(col) && new Set(Object.values(colors)).size < need) {
|
|
6752
|
+
const next = unused(orderOf(c));
|
|
6753
|
+
if (next) colors[c] = next;
|
|
6754
|
+
}
|
|
6755
|
+
if (colors[c]) seen.add(colors[c]);
|
|
6756
|
+
}
|
|
6757
|
+
for (const c of choices) {
|
|
6758
|
+
if (colors[c]) continue;
|
|
6759
|
+
colors[c] = unused(orderOf(c)) ?? orderOf(c)[choices.indexOf(c) % orderOf(c).length];
|
|
6760
|
+
}
|
|
6761
|
+
for (const c of choices) {
|
|
6762
|
+
const wrong = GOOD_OUTCOME.test(c) ? "red" : BAD_OUTCOME.test(c) ? "green" : null;
|
|
6763
|
+
if (!wrong || colors[c] !== wrong) continue;
|
|
6764
|
+
const want = GOOD_OUTCOME.test(c) ? "green" : "red";
|
|
6765
|
+
const other = holder(want, c);
|
|
6766
|
+
if (!other) colors[c] = want;
|
|
6767
|
+
else if (!(want === "red" ? GOOD_OUTCOME.test(other) : BAD_OUTCOME.test(other))) {
|
|
6768
|
+
colors[other] = wrong;
|
|
6769
|
+
colors[c] = want;
|
|
6770
|
+
} else colors[c] = unused(orderOf(c).filter((x) => x !== wrong)) ?? colors[c];
|
|
6771
|
+
}
|
|
6772
|
+
const ordered = {};
|
|
6773
|
+
for (const c of choices) ordered[c] = colors[c];
|
|
6774
|
+
const was = f.choiceColors ?? {};
|
|
6775
|
+
if (Object.keys(was).length !== choices.length || choices.some((c) => was[c] !== ordered[c])) {
|
|
6776
|
+
f.choiceColors = ordered;
|
|
6777
|
+
fixes.push(`tables.${t.token}.fields.${f.key}: choice colours filled and checked (good never red, bad never green)`);
|
|
6778
|
+
}
|
|
6779
|
+
}
|
|
6780
|
+
}
|
|
6781
|
+
}
|
|
6782
|
+
function fixRelationships(spec, ctx, fixes) {
|
|
6783
|
+
const everyTable = allTables(spec);
|
|
6784
|
+
const byToken = new Map(everyTable.map((t) => [t.token, t]));
|
|
6785
|
+
const rels = spec.relationships ?? [];
|
|
6786
|
+
for (const t of spec.tables) {
|
|
6787
|
+
for (const f of t.fields) {
|
|
6788
|
+
if (f.parityType !== "relation" || !f.relationTarget) continue;
|
|
6789
|
+
if (rels.some((r) => r.fromTable === t.token && r.fromField === f.key)) continue;
|
|
6790
|
+
const target = byToken.get(f.relationTarget);
|
|
6791
|
+
const toName = lower(target?.labelSingular ?? f.relationTarget.replace(/_/g, " "));
|
|
6792
|
+
const fromName = lower(t.labelSingular ?? t.token.replace(/_/g, " "));
|
|
6793
|
+
const many = !!f.multi || f.relationMax !== void 0 && f.relationMax !== 1;
|
|
6794
|
+
const planned = ctx.plan?.tables.find((p) => p.token === t.token && p.links.some((l) => l.key === f.key))?.purpose?.trim();
|
|
6795
|
+
const describes = planned && planned.length >= 15 && planned.length <= 140 ? planned : many ? `A ${fromName} lists its ${toName} records.` : `A ${fromName} belongs to one ${toName}.`;
|
|
6796
|
+
const taken = /* @__PURE__ */ new Set([...rels.map((r) => r.inverseKey), ...(target?.fields ?? []).map((x) => x.key)]);
|
|
6797
|
+
let inverseKey = `${t.token}s`;
|
|
6798
|
+
if (taken.has(inverseKey)) inverseKey = `${t.token}_${f.key}s`;
|
|
6799
|
+
for (let i = 2; taken.has(inverseKey); i++) inverseKey = `${t.token}_${f.key}s_${i}`;
|
|
6800
|
+
const toPerson = target?.subject === "person" && f.relationTarget !== "team_member";
|
|
6801
|
+
rels.push({ describes, fromTable: t.token, fromField: f.key, toTable: f.relationTarget, flavor: "referenced", cardinality: many ? "many" : "one", onDelete: toPerson ? "restrict" : "set_null", inverseKey });
|
|
6802
|
+
fixes.push(`relationships: stated ${t.token}.${f.key} \u2192 ${f.relationTarget}`);
|
|
6803
|
+
}
|
|
6804
|
+
}
|
|
6805
|
+
spec.relationships = rels;
|
|
6806
|
+
}
|
|
6807
|
+
function askRequired(spec, fixes) {
|
|
6808
|
+
for (const form of spec.forms) {
|
|
6809
|
+
const t = spec.tables.find((x) => x.token === form.table);
|
|
6810
|
+
if (!t) continue;
|
|
6811
|
+
const asked = new Set((form.fields ?? []).map((q2) => q2.fieldKey));
|
|
6812
|
+
for (const f of t.fields) {
|
|
6813
|
+
if (!f.required || isComputed2(f) || f.default !== void 0 || asked.has(f.key)) continue;
|
|
6814
|
+
form.fields = [...form.fields ?? [], { fieldKey: f.key, ask: `What is the ${lower(f.label)}?`, required: true }];
|
|
6815
|
+
fixes.push(`forms.${form.token}.fields.${f.key}: asked (a required field no question covered); reword in repair`);
|
|
6816
|
+
}
|
|
6817
|
+
}
|
|
6818
|
+
}
|
|
6819
|
+
var REALISM = ["childDates", "withinParent", "statusAgrees", "countMatches", "datePairs"];
|
|
6820
|
+
function switchJob(spec, fixes) {
|
|
6821
|
+
if (spec.job !== "crm_pipeline" && spec.job !== "billing") return;
|
|
6822
|
+
const linked = (spec.relationships ?? []).length > 0 || spec.tables.some((t) => t.fields.some((f) => f.parityType === "relation" && f.relationTarget && spec.tables.some((x) => x.token === f.relationTarget)));
|
|
6823
|
+
if (!linked) return;
|
|
6824
|
+
const declared = spec.tables.some((t) => REALISM.some((k) => Array.isArray(t[k]) && t[k].length > 0));
|
|
6825
|
+
if (declared) return;
|
|
6826
|
+
const to = spec.job === "crm_pipeline" ? "work_orders" : "intake";
|
|
6827
|
+
fixes.push(`job: ${spec.job} switched to ${to} (linked tables, no realism declarations)`);
|
|
6828
|
+
spec.job = to;
|
|
6829
|
+
}
|
|
6830
|
+
function autofixDescribeSpec(input, ctx = {}) {
|
|
6831
|
+
const spec = describeSpec(clone2(input));
|
|
6832
|
+
const autoFixes = [];
|
|
6833
|
+
raiseSensitivity(spec, autoFixes);
|
|
6834
|
+
flagAskedOfSubject(spec, ctx, autoFixes);
|
|
6835
|
+
fixBooking(spec, autoFixes);
|
|
6836
|
+
fixAvailability(spec, autoFixes);
|
|
6837
|
+
fixViewTokens(spec, autoFixes);
|
|
6838
|
+
fixColours(spec, autoFixes);
|
|
6839
|
+
fixRelationships(spec, ctx, autoFixes);
|
|
6840
|
+
askRequired(spec, autoFixes);
|
|
6841
|
+
switchJob(spec, autoFixes);
|
|
6842
|
+
return { spec, autoFixes };
|
|
6843
|
+
}
|
|
6844
|
+
function describeCheck(spec, ctx = {}) {
|
|
6845
|
+
const fixed = autofixDescribeSpec(spec, ctx);
|
|
6846
|
+
const v = validateTemplate(fixed.spec, { profile: "describe" });
|
|
6847
|
+
return { ...fixed, problems: v.problems.map((p) => ({ at: p.at, says: p.says })) };
|
|
6848
|
+
}
|
|
6849
|
+
|
|
6850
|
+
// src/templates/describe/repair.ts
|
|
6851
|
+
var clone3 = (v) => JSON.parse(JSON.stringify(v));
|
|
6852
|
+
function parseAt(at) {
|
|
6853
|
+
const out = [];
|
|
6854
|
+
const re = /([A-Za-z_][A-Za-z0-9_]*)(?:\[(\d+)\])?(?: \(([^)]*)\))?/g;
|
|
6855
|
+
for (let m = re.exec(at); m; m = re.exec(at)) {
|
|
6856
|
+
const named = m[3] !== void 0 && m[3] !== "" && m[3] !== "undefined" && m[3] !== "null";
|
|
6857
|
+
out.push({ root: m[1], ...named ? { id: m[3] } : m[2] !== void 0 ? { id: `#${m[2]}` } : {} });
|
|
6858
|
+
}
|
|
6859
|
+
return out;
|
|
6860
|
+
}
|
|
6861
|
+
function pathOfAt(at) {
|
|
6862
|
+
const segs = parseAt(at);
|
|
6863
|
+
const [a, b] = segs;
|
|
6864
|
+
if (!a) return "spec";
|
|
6865
|
+
if ((a.root === "tables" || a.root === "forms") && a.id !== void 0) {
|
|
6866
|
+
if (b && (b.root === "fields" || b.root === "questions") && b.id) return `${a.root}.${a.id}.fields.${b.id}`;
|
|
6867
|
+
return `${a.root}.${a.id}`;
|
|
6868
|
+
}
|
|
6869
|
+
if (a.root === "extras" && a.id) {
|
|
6870
|
+
if (b && b.root === "questions" && b.id) return `extras.${a.id}.questions.${b.id}`;
|
|
6871
|
+
return b && b.root === "availability" ? `extras.${a.id}.availability` : `extras.${a.id}`;
|
|
6872
|
+
}
|
|
6873
|
+
if (a.root === "views" && a.id) return `views.${a.id}`;
|
|
6874
|
+
if (a.root === "business") return "business";
|
|
6875
|
+
return "spec";
|
|
6876
|
+
}
|
|
6877
|
+
var idOf = (o) => typeof o.key === "string" ? o.key : typeof o.fieldKey === "string" ? o.fieldKey : typeof o.token === "string" ? o.token : void 0;
|
|
6878
|
+
function findIn(list, id) {
|
|
6879
|
+
if (!Array.isArray(list)) return void 0;
|
|
6880
|
+
if (/^#\d+$/.test(id)) return list[Number(id.slice(1))];
|
|
6881
|
+
return list.find((x) => x && idOf(x) === id);
|
|
6882
|
+
}
|
|
6883
|
+
function resolvePath(spec, path) {
|
|
6884
|
+
const s = spec;
|
|
6885
|
+
const parts = path.split(".");
|
|
6886
|
+
const [root, id, sub, subId] = parts;
|
|
6887
|
+
if (root === "spec" || !root) return s;
|
|
6888
|
+
if (root === "business") return s.business;
|
|
6889
|
+
if (root === "relationships") {
|
|
6890
|
+
const rels = s.relationships ?? [];
|
|
6891
|
+
return rels.find((r) => r.fromTable === id && (parts[2] === void 0 || r.fromField === parts[2]));
|
|
6892
|
+
}
|
|
6893
|
+
let item;
|
|
6894
|
+
if (root === "extras") item = findIn(s.extras, id ?? "") ?? (s.extras ?? []).find((e) => e.kind === id);
|
|
6895
|
+
else if (root === "tables" || root === "forms" || root === "views") item = findIn(s[root], id ?? "");
|
|
6896
|
+
if (!item) return void 0;
|
|
6897
|
+
if (!sub) return item;
|
|
6898
|
+
if (sub === "availability") return item.availability;
|
|
6899
|
+
if ((sub === "fields" || sub === "questions") && subId) return findIn(item[sub], subId);
|
|
6900
|
+
return void 0;
|
|
6901
|
+
}
|
|
6902
|
+
function specBrief(spec, problems) {
|
|
6903
|
+
const s = spec;
|
|
6904
|
+
const brief = { job: spec.job, industry: spec.industry, vertical: spec.vertical, business: spec.business, tables: spec.tables.map((t) => t.token), forms: spec.forms.map((f) => f.token), views: spec.views.map((v) => v.token) };
|
|
6905
|
+
for (const p of problems) {
|
|
6906
|
+
const root = parseAt(p.at)[0]?.root;
|
|
6907
|
+
if (root && root !== "spec" && root in s && !(root in brief)) brief[root] = clone3(s[root]);
|
|
6908
|
+
}
|
|
6909
|
+
return brief;
|
|
6910
|
+
}
|
|
6911
|
+
function tableBrief(t) {
|
|
6912
|
+
const { fields, rows: _rows, ...own } = t;
|
|
6913
|
+
return { ...own, fields: fields.map((f) => ({ key: f.key, label: f.label, parityType: f.parityType, ...f.choices ? { choices: f.choices } : {}, ...f.relationTarget ? { relationTarget: f.relationTarget } : {}, ...f.required ? { required: true } : {} })) };
|
|
6914
|
+
}
|
|
6915
|
+
function repairSlices(spec, problems) {
|
|
6916
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
6917
|
+
for (const p of problems) {
|
|
6918
|
+
const first = parseAt(p.at)[0];
|
|
6919
|
+
const rel = first?.root === "relationships" && first.id !== void 0 && /^#\d+$/.test(first.id) ? spec.relationships?.[Number(first.id.slice(1))] : void 0;
|
|
6920
|
+
const path = rel ? `relationships.${rel.fromTable}.${rel.fromField}` : pathOfAt(p.at);
|
|
6921
|
+
let slice = byPath.get(path);
|
|
6922
|
+
if (!slice) {
|
|
6923
|
+
const found = resolvePath(spec, path);
|
|
6924
|
+
const isTable = path.startsWith("tables.") && path.split(".").length === 2;
|
|
6925
|
+
const value = path === "spec" ? specBrief(spec, problems.filter((q2) => pathOfAt(q2.at) === "spec")) : isTable && found ? tableBrief(found) : found ? clone3(found) : null;
|
|
6926
|
+
slice = { path, slice: value, problems: [] };
|
|
6927
|
+
byPath.set(path, slice);
|
|
6928
|
+
}
|
|
6929
|
+
slice.problems.push({ at: p.at, says: p.says });
|
|
6930
|
+
}
|
|
6931
|
+
return [...byPath.values()];
|
|
6932
|
+
}
|
|
6933
|
+
var IMMUTABLE = /* @__PURE__ */ new Set(["key", "token", "fieldKey"]);
|
|
6934
|
+
function applyRepairPatches(input, patches) {
|
|
6935
|
+
const spec = clone3(input);
|
|
6936
|
+
const applied = [];
|
|
6937
|
+
const skipped = [];
|
|
6938
|
+
for (const patch of patches ?? []) {
|
|
6939
|
+
const path = String(patch?.path ?? "");
|
|
6940
|
+
const set = patch?.set;
|
|
6941
|
+
if (!path || !set || typeof set !== "object" || Array.isArray(set)) {
|
|
6942
|
+
skipped.push({ path, why: "a patch needs a path and a set object" });
|
|
6943
|
+
continue;
|
|
6944
|
+
}
|
|
6945
|
+
let target = resolvePath(spec, path);
|
|
6946
|
+
if (!target) {
|
|
6947
|
+
const parts = path.split(".");
|
|
6948
|
+
const parent = resolvePath(spec, parts.slice(0, 2).join("."));
|
|
6949
|
+
const [root, , sub, subId] = parts;
|
|
6950
|
+
if (parent && root === "tables" && sub === "fields" && subId && parts.length === 4 && typeof set.label === "string" && typeof set.parityType === "string") {
|
|
6951
|
+
const f = { key: subId, sensitivity: "internal", contextPolicy: "include", ...set };
|
|
6952
|
+
parent.fields.push(f);
|
|
6953
|
+
target = f;
|
|
6954
|
+
applied.push(path);
|
|
6955
|
+
continue;
|
|
6956
|
+
}
|
|
6957
|
+
if (parent && root === "forms" && sub === "fields" && subId && parts.length === 4 && typeof set.ask === "string") {
|
|
6958
|
+
(parent.fields ??= []).push({ fieldKey: subId, ...set });
|
|
6959
|
+
applied.push(path);
|
|
6960
|
+
continue;
|
|
6961
|
+
}
|
|
6962
|
+
skipped.push({ path, why: "no such piece in the spec" });
|
|
6963
|
+
continue;
|
|
6964
|
+
}
|
|
6965
|
+
for (const [k, v] of Object.entries(set)) {
|
|
6966
|
+
if (IMMUTABLE.has(k) && v !== target[k]) {
|
|
6967
|
+
skipped.push({ path: `${path}.${k}`, why: `${k} is never renamed by a patch` });
|
|
6968
|
+
continue;
|
|
6969
|
+
}
|
|
6970
|
+
if (v === null) delete target[k];
|
|
6971
|
+
else target[k] = v;
|
|
6972
|
+
}
|
|
6973
|
+
applied.push(path);
|
|
6974
|
+
}
|
|
6975
|
+
return { spec, applied, skipped };
|
|
6976
|
+
}
|
|
6977
|
+
function lastResortTrim(input, problems) {
|
|
6978
|
+
const spec = clone3(input);
|
|
6979
|
+
const notes = [];
|
|
6980
|
+
const droppedViews = /* @__PURE__ */ new Set();
|
|
6981
|
+
const views = spec.views;
|
|
6982
|
+
for (const p of problems) {
|
|
6983
|
+
const segs = parseAt(p.at);
|
|
6984
|
+
const [a, b] = segs;
|
|
6985
|
+
const last = segs[segs.length - 1];
|
|
6986
|
+
if (!a || !a.id) continue;
|
|
6987
|
+
if (a.root === "views") {
|
|
6988
|
+
const v = findIn(views, a.id);
|
|
6989
|
+
if (v && views.length - droppedViews.size > 1 && !droppedViews.has(v)) {
|
|
6990
|
+
droppedViews.add(v);
|
|
6991
|
+
notes.push(`Left out the view "${v.name}": it did not pass the store's check.`);
|
|
6992
|
+
}
|
|
6993
|
+
continue;
|
|
6994
|
+
}
|
|
6995
|
+
if (last?.root === "help") {
|
|
6996
|
+
const holder = a.root === "tables" ? resolvePath(spec, `tables.${a.id}.fields.${b?.id ?? ""}`) : a.root === "forms" ? resolvePath(spec, `forms.${a.id}.fields.${b?.id ?? ""}`) : a.root === "extras" ? resolvePath(spec, `extras.${a.id}.questions.${b?.id ?? ""}`) : void 0;
|
|
6997
|
+
if (holder && "help" in holder) {
|
|
6998
|
+
delete holder.help;
|
|
6999
|
+
notes.push(`Left out the help text on "${b?.id}": it did not pass the store's check.`);
|
|
7000
|
+
}
|
|
7001
|
+
continue;
|
|
7002
|
+
}
|
|
7003
|
+
if (last?.root === "ask" && b?.id) {
|
|
7004
|
+
const holder = a.root === "forms" ? findIn(spec.forms, a.id) : a.root === "extras" ? findIn(spec.extras, a.id) : void 0;
|
|
7005
|
+
const list = (a.root === "forms" ? holder?.fields : holder?.questions) ?? null;
|
|
7006
|
+
const table = holder?.table;
|
|
7007
|
+
const field2 = spec.tables.find((t) => t.token === table)?.fields.find((f) => f.key === b.id);
|
|
7008
|
+
const q2 = list?.find((x) => x.fieldKey === b.id);
|
|
7009
|
+
if (list && q2 && !q2.required && !field2?.required) {
|
|
7010
|
+
list.splice(list.indexOf(q2), 1);
|
|
7011
|
+
notes.push(`Left out the question on "${b.id}": it did not pass the store's check.`);
|
|
7012
|
+
}
|
|
7013
|
+
}
|
|
7014
|
+
}
|
|
7015
|
+
if (droppedViews.size) spec.views = spec.views.filter((x) => !droppedViews.has(x));
|
|
7016
|
+
return { spec, notes };
|
|
7017
|
+
}
|
|
7018
|
+
|
|
7019
|
+
// src/templates/describe/package.ts
|
|
7020
|
+
var WORTH_SAYING = /^job:|: added |: asked |: dropped |dropped \d+ question/;
|
|
7021
|
+
var NOTES_MAX = 7;
|
|
7022
|
+
function packageDescribeResult(spec, plan, autoFixes, reuses, extraNotes = []) {
|
|
7023
|
+
const tokens = new Set(spec.tables.map((t) => t.token));
|
|
7024
|
+
const left = plan?.leaveOut?.length ? [`Left out: ${plan.leaveOut.join("; ")}.`] : [];
|
|
7025
|
+
const said = autoFixes.filter((f) => WORTH_SAYING.test(f)).map((f) => f.startsWith("job:") ? `Used a simpler setup type: ${f.slice(5)}.` : f);
|
|
7026
|
+
const notes = [...plan?.assumptions ?? [], ...left, ...said, ...extraNotes].map((n) => n.trim()).filter(Boolean).slice(0, NOTES_MAX);
|
|
7027
|
+
return {
|
|
7028
|
+
__kind: "describe_template_result",
|
|
7029
|
+
template: JSON.parse(JSON.stringify(spec)),
|
|
7030
|
+
notes,
|
|
7031
|
+
reuses: (reuses ?? []).filter((r) => r && tokens.has(r.token))
|
|
7032
|
+
};
|
|
7033
|
+
}
|
|
7034
|
+
|
|
7035
|
+
// src/templates/describe/vocabulary.ts
|
|
7036
|
+
var BOOKING_AVAILABILITY_FACTS = {
|
|
7037
|
+
timezone: "text \u2014 an IANA time zone such as America/Los_Angeles; defaults to the business's",
|
|
7038
|
+
slotMinutes: "one whole number 5 to 480 \u2014 how long one appointment is, in minutes",
|
|
7039
|
+
bufferMinutes: "one whole number 0 to 480 \u2014 rest between appointments, in minutes",
|
|
7040
|
+
leadMinutes: "one whole number 0 to 43200 \u2014 how much notice a person must give, in minutes",
|
|
7041
|
+
maxPerDay: "one whole number 1 to 100 \u2014 appointments accepted in one day",
|
|
7042
|
+
days: "how many days ahead a person may book \u2014 ONE whole number such as 21 or 28 (1 to 90); weekdays belong in windows, never here",
|
|
7043
|
+
windows: "list of {weekday, from, to}: weekday one whole number 0 (Sunday) to 6 (Saturday), from and to written HH:MM with to later than from; one entry per weekday offered"
|
|
7044
|
+
};
|
|
7045
|
+
var VIEW_SHAPE_FACTS = {
|
|
7046
|
+
token: "snake_case, unique among every view, form and extra \u2014 always written",
|
|
7047
|
+
name: 'what a person clicks, such as "Intake board"',
|
|
7048
|
+
table: "the token of one of the template's tables",
|
|
7049
|
+
kind: TEMPLATE_VIEW_KINDS,
|
|
7050
|
+
groupBy: "kanban only, required: the key of a select field whose choices become the columns",
|
|
7051
|
+
dateField: "calendar only, required: the key of a datetime field",
|
|
7052
|
+
startField_endField: "timeline only, required: keys of two datetime fields",
|
|
7053
|
+
coverField: "gallery only, required: the key of a file field"
|
|
7054
|
+
};
|
|
7055
|
+
function describeVocabulary() {
|
|
7056
|
+
return {
|
|
7057
|
+
field_kinds: RESOLVABLE_FIELD_TYPE_WORDS,
|
|
7058
|
+
table_types: TABLE_TYPES,
|
|
7059
|
+
table_displays: TABLE_DISPLAYS,
|
|
7060
|
+
storage_modes: STORAGE_MODES,
|
|
7061
|
+
field_sensitivities: FIELD_SENSITIVITIES,
|
|
7062
|
+
context_policies: CONTEXT_POLICIES,
|
|
7063
|
+
field_sources: FIELD_SOURCES,
|
|
7064
|
+
relation_flavors: RELATION_FLAVORS,
|
|
7065
|
+
relation_cardinalities: RELATION_CARDINALITIES,
|
|
7066
|
+
on_delete: ON_DELETE,
|
|
7067
|
+
industry_groups: INDUSTRY_GROUPS,
|
|
7068
|
+
template_jobs: TEMPLATE_JOBS,
|
|
7069
|
+
teaches_features: TEACHES_FEATURES,
|
|
7070
|
+
choice_colors: CHOICE_COLORS,
|
|
7071
|
+
template_view_kinds: TEMPLATE_VIEW_KINDS,
|
|
7072
|
+
template_audiences: TEMPLATE_AUDIENCES,
|
|
7073
|
+
form_audiences: ["public", "member", "staff"],
|
|
7074
|
+
extra_kinds_for_describe: ["booking"],
|
|
7075
|
+
booking_window: { weekday: "0 = Sunday \u2026 6 = Saturday", from: "HH:MM", to: "HH:MM later than from" },
|
|
7076
|
+
booking_availability: BOOKING_AVAILABILITY_FACTS,
|
|
7077
|
+
view_shape: VIEW_SHAPE_FACTS,
|
|
7078
|
+
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"
|
|
7079
|
+
};
|
|
7080
|
+
}
|
|
6353
7081
|
export {
|
|
7082
|
+
BAD_OUTCOME,
|
|
7083
|
+
BOOKING_AVAILABILITY_FACTS,
|
|
6354
7084
|
CAPABILITY_WORDS,
|
|
6355
7085
|
CATALOGUE_QUOTAS,
|
|
6356
7086
|
CATALOGUE_STATUSES,
|
|
@@ -6360,6 +7090,8 @@ export {
|
|
|
6360
7090
|
DESIGNATED_REAL_NUMBERS,
|
|
6361
7091
|
ENTITY_REFERENCE_KINDS,
|
|
6362
7092
|
FORMULA_FUNCTIONS,
|
|
7093
|
+
GALLERY_ONLY,
|
|
7094
|
+
GOOD_OUTCOME,
|
|
6363
7095
|
INDUSTRY_GROUPS,
|
|
6364
7096
|
INSTALL_PLAN_VERSION,
|
|
6365
7097
|
MAIN_TABLE_ROWS_MAX,
|
|
@@ -6387,11 +7119,14 @@ export {
|
|
|
6387
7119
|
TEMPLATE_VIEW_KINDS,
|
|
6388
7120
|
TIME_BOUND_STAGE,
|
|
6389
7121
|
TITLE_DISTINCT_SHARE,
|
|
7122
|
+
VIEW_SHAPE_FACTS,
|
|
6390
7123
|
agentCopyRequest,
|
|
6391
7124
|
allTables,
|
|
6392
7125
|
anyOfWhere,
|
|
7126
|
+
applyRepairPatches,
|
|
6393
7127
|
assertTemplate,
|
|
6394
7128
|
attachmentProblems,
|
|
7129
|
+
autofixDescribeSpec,
|
|
6395
7130
|
automationHeld,
|
|
6396
7131
|
blockTable,
|
|
6397
7132
|
bookingArgs,
|
|
@@ -6408,12 +7143,16 @@ export {
|
|
|
6408
7143
|
deferredRelationFields,
|
|
6409
7144
|
deferredRelations,
|
|
6410
7145
|
derivedFieldSpec,
|
|
7146
|
+
describeCheck,
|
|
7147
|
+
describeSpec,
|
|
7148
|
+
describeVocabulary,
|
|
6411
7149
|
digestSpec,
|
|
6412
7150
|
documentBody,
|
|
6413
7151
|
domainsIn,
|
|
6414
7152
|
drillDownDashboard,
|
|
6415
7153
|
evalFormula,
|
|
6416
7154
|
extrasProblems,
|
|
7155
|
+
fallbackView,
|
|
6417
7156
|
footprintOf,
|
|
6418
7157
|
foreignTextProblems,
|
|
6419
7158
|
formArgs,
|
|
@@ -6423,13 +7162,18 @@ export {
|
|
|
6423
7162
|
foundationTokens,
|
|
6424
7163
|
isSelfDeclaredField,
|
|
6425
7164
|
isTimedField,
|
|
7165
|
+
lastResortTrim,
|
|
6426
7166
|
membersAddRowsSteps,
|
|
7167
|
+
mergeDescribeParts,
|
|
6427
7168
|
normalizeForPhones,
|
|
6428
7169
|
notificationParts,
|
|
6429
7170
|
numberText,
|
|
6430
7171
|
numberedStreet,
|
|
7172
|
+
packageDescribeResult,
|
|
7173
|
+
parseAt,
|
|
6431
7174
|
parseFormula,
|
|
6432
7175
|
parseNumeric,
|
|
7176
|
+
pathOfAt,
|
|
6433
7177
|
peopleOf,
|
|
6434
7178
|
phonesOutsideFictionalRange,
|
|
6435
7179
|
planTables,
|
|
@@ -6437,9 +7181,12 @@ export {
|
|
|
6437
7181
|
relationFieldSpec,
|
|
6438
7182
|
relationPatches,
|
|
6439
7183
|
relativeHasTime,
|
|
7184
|
+
repairSlices,
|
|
6440
7185
|
requiredCircleProblem,
|
|
6441
7186
|
resolveDateToken,
|
|
7187
|
+
resolvePath,
|
|
6442
7188
|
resolveRelativeDates,
|
|
7189
|
+
reusesOf,
|
|
6443
7190
|
rowActionList,
|
|
6444
7191
|
rowLevels,
|
|
6445
7192
|
rowsFor,
|