@superpanel-sdk/runtime 0.2.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -8
- package/dist/builder/codegen/emit.d.ts +29 -11
- package/dist/builder/codegen/emit.js +328 -61
- package/dist/builder/codegen/generate.d.ts +21 -0
- package/dist/builder/codegen/generate.js +152 -20
- package/dist/builder/company-import-graph.d.ts +2 -0
- package/dist/builder/company-import-graph.js +40 -8
- package/dist/builder/company-imports.d.ts +24 -0
- package/dist/builder/company-imports.js +114 -0
- package/dist/builder/compile/compile-sequence.d.ts +59 -8
- package/dist/builder/compile/compile-sequence.js +279 -77
- package/dist/builder/compile/function-code.d.ts +12 -2
- package/dist/builder/compile/function-code.js +87 -39
- package/dist/builder/compile/registry.d.ts +7 -3
- package/dist/builder/compile/registry.js +24 -23
- package/dist/builder/load.d.ts +70 -10
- package/dist/builder/load.js +250 -61
- package/dist/builder/paths.d.ts +3 -0
- package/dist/builder/paths.js +16 -0
- package/dist/builder/schedules.d.ts +23 -0
- package/dist/builder/schedules.js +102 -0
- package/dist/builder/slot-schema-validation.d.ts +4 -0
- package/dist/builder/slot-schema-validation.js +10 -0
- package/dist/builder/typecheck.d.ts +8 -0
- package/dist/builder/typecheck.js +105 -0
- package/dist/builder/types.d.ts +91 -19
- package/dist/builder/types.js +2 -1
- package/dist/cli/auth/auth.d.ts +37 -0
- package/dist/cli/auth/auth.js +211 -0
- package/dist/cli/auth/backend-url.d.ts +5 -0
- package/dist/cli/auth/backend-url.js +25 -0
- package/dist/cli/auth/clipboard.d.ts +9 -0
- package/dist/cli/auth/clipboard.js +38 -0
- package/dist/cli/auth/http.d.ts +20 -0
- package/dist/cli/auth/http.js +27 -0
- package/dist/cli/auth/index.d.ts +10 -0
- package/dist/cli/auth/index.js +27 -0
- package/dist/cli/auth/progress.d.ts +12 -0
- package/dist/cli/auth/progress.js +15 -0
- package/dist/cli/auth/serve-login.d.ts +8 -0
- package/dist/cli/auth/serve-login.js +24 -0
- package/dist/cli/compile/index.js +3 -0
- package/dist/cli/config.d.ts +10 -0
- package/dist/cli/config.js +52 -0
- package/dist/cli/index.js +28 -8
- package/dist/cli/serve/index.d.ts +73 -3
- package/dist/cli/serve/index.js +365 -116
- package/dist/cli/type-gen/index.js +9 -8
- package/dist/compiler.d.ts +7 -0
- package/dist/compiler.js +7 -0
- package/dist/compute-host/compute-host.d.ts +15 -23
- package/dist/compute-host/compute-host.js +25 -28
- package/dist/compute-host/index.d.ts +2 -4
- package/dist/compute-host/index.js +1 -2
- package/dist/compute-host/source-compute-host.d.ts +11 -9
- package/dist/compute-host/source-compute-host.js +46 -32
- package/dist/core/case-orchestrator/contact-hours.d.ts +50 -0
- package/dist/core/case-orchestrator/contact-hours.js +108 -0
- package/dist/core/case-orchestrator/follow-ups.d.ts +31 -0
- package/dist/core/case-orchestrator/follow-ups.js +103 -0
- package/dist/core/case-orchestrator/index.d.ts +6 -0
- package/dist/core/case-orchestrator/index.js +6 -0
- package/dist/core/case-orchestrator/render.d.ts +75 -0
- package/dist/core/case-orchestrator/render.js +239 -0
- package/dist/core/case-orchestrator/time.d.ts +11 -0
- package/dist/core/case-orchestrator/time.js +29 -0
- package/dist/core/case-orchestrator/tools.d.ts +29 -0
- package/dist/core/case-orchestrator/tools.js +31 -0
- package/dist/core/case-orchestrator/types.d.ts +753 -0
- package/dist/core/case-orchestrator/types.js +46 -0
- package/dist/core/case-orchestrator/validate.d.ts +11 -0
- package/dist/core/case-orchestrator/validate.js +63 -0
- package/dist/core/context/call.d.ts +291 -0
- package/dist/core/context/call.js +94 -0
- package/dist/core/context/channels.d.ts +60 -0
- package/dist/core/context/channels.js +35 -0
- package/dist/core/context/contact.d.ts +96 -6
- package/dist/core/context/contact.js +48 -2
- package/dist/core/context/email-message.d.ts +159 -0
- package/dist/core/context/email-message.js +59 -0
- package/dist/core/context/email.d.ts +97 -0
- package/dist/core/context/email.js +91 -0
- package/dist/core/context/exchanges.d.ts +142 -0
- package/dist/core/context/exchanges.js +63 -0
- package/dist/core/context/instances.d.ts +278 -0
- package/dist/core/context/instances.js +123 -0
- package/dist/core/context/kv.d.ts +113 -0
- package/dist/core/context/kv.js +60 -0
- package/dist/core/context/platform-registry.d.ts +509 -4
- package/dist/core/context/platform-registry.js +14 -2
- package/dist/core/context/platform.d.ts +2 -1
- package/dist/core/context/sms.d.ts +147 -0
- package/dist/core/context/sms.js +57 -0
- package/dist/core/context/types.d.ts +51 -48
- package/dist/core/context/types.js +1 -7
- package/dist/core/conversation/index.d.ts +31 -0
- package/dist/core/conversation/index.js +14 -0
- package/dist/core/functions/define/bind.d.ts +25 -0
- package/dist/core/functions/define/bind.js +14 -0
- package/dist/core/functions/define/case.d.ts +9 -0
- package/dist/core/functions/define/case.js +13 -0
- package/dist/core/functions/define/events.d.ts +20 -0
- package/dist/core/functions/define/events.js +21 -0
- package/dist/core/functions/define/no-case.d.ts +4 -0
- package/dist/core/functions/define/no-case.js +9 -0
- package/dist/core/functions/events.d.ts +247 -0
- package/dist/core/functions/events.js +32 -0
- package/dist/core/functions/func-ref.d.ts +11 -2
- package/dist/core/functions/schedules.d.ts +51 -0
- package/dist/core/functions/schedules.js +20 -0
- package/dist/core/functions/types.d.ts +62 -51
- package/dist/core/functions/types.js +6 -1
- package/dist/core/office-hours/index.d.ts +39 -0
- package/dist/core/office-hours/index.js +187 -0
- package/dist/core/page/define.d.ts +834 -0
- package/dist/core/page/define.js +477 -0
- package/dist/core/page/ops.d.ts +58 -0
- package/dist/core/page/ops.js +220 -0
- package/dist/core/page/render.d.ts +253 -0
- package/dist/core/page/render.js +840 -0
- package/dist/core/page/time-format.d.ts +19 -0
- package/dist/core/page/time-format.js +41 -0
- package/dist/core/page/timeline.d.ts +40 -0
- package/dist/core/page/timeline.js +2 -0
- package/dist/core/page/typed.d.ts +642 -0
- package/dist/core/page/typed.js +2 -0
- package/dist/core/page/workspace.d.ts +33 -0
- package/dist/core/page/workspace.js +231 -0
- package/dist/core/party/index.d.ts +3 -0
- package/dist/core/party/index.js +3 -0
- package/dist/core/party/schema.d.ts +137 -0
- package/dist/core/party/schema.js +80 -0
- package/dist/core/party/types.d.ts +40 -0
- package/dist/core/party/types.js +15 -0
- package/dist/core/sequence/define.d.ts +57 -0
- package/dist/core/sequence/define.js +10 -0
- package/dist/core/sequence/index.d.ts +63 -0
- package/dist/core/sequence/index.js +231 -0
- package/dist/core/slot/binding.d.ts +40 -0
- package/dist/core/slot/binding.js +2 -0
- package/dist/core/slot/company-metadata.d.ts +113 -0
- package/dist/core/slot/company-metadata.js +563 -0
- package/dist/core/slot/compile-schema.d.ts +19 -13
- package/dist/core/slot/compile-schema.js +51 -248
- package/dist/core/slot/contact-timezone.d.ts +110 -0
- package/dist/core/slot/contact-timezone.js +186 -0
- package/dist/core/slot/field.d.ts +2 -24
- package/dist/core/slot/field.js +0 -69
- package/dist/core/slot/id-ref.d.ts +17 -2
- package/dist/core/slot/id-ref.js +16 -8
- package/dist/core/slot/index.d.ts +12 -13
- package/dist/core/slot/index.js +11 -12
- package/dist/core/slot/naming.d.ts +15 -0
- package/dist/core/slot/naming.js +96 -0
- package/dist/core/slot/platform-fields.d.ts +119 -0
- package/dist/core/slot/platform-fields.js +121 -0
- package/dist/core/slot/ref.d.ts +16 -0
- package/dist/core/slot/ref.js +14 -0
- package/dist/core/slot/row.d.ts +2 -9
- package/dist/core/slot/row.js +13 -23
- package/dist/core/slot/schema-compat.d.ts +16 -0
- package/dist/core/slot/schema-compat.js +112 -0
- package/dist/core/slot/schema.d.ts +409 -76
- package/dist/core/slot/schema.js +178 -10
- package/dist/core/slot/types.d.ts +27 -94
- package/dist/core/slot/value.d.ts +26 -3
- package/dist/core/slot/value.js +89 -57
- package/dist/dev-overlay/protocol.d.ts +41 -8
- package/dist/fn-context/build-context.d.ts +3 -1
- package/dist/fn-context/build-context.js +20 -10
- package/dist/fn-context/fake/empty-api.js +9 -28
- package/dist/fn-context/fake/index.d.ts +11 -4
- package/dist/fn-context/fake/index.js +27 -2
- package/dist/fn-context/fn-params.d.ts +6 -0
- package/dist/fn-context/fn-params.js +13 -0
- package/dist/fn-context/index.d.ts +5 -5
- package/dist/fn-context/index.js +4 -4
- package/dist/fn-context/log.js +2 -2
- package/dist/fn-context/make-ctx.d.ts +11 -0
- package/dist/fn-context/make-ctx.js +18 -8
- package/dist/fn-context/rpc/case-orchestrator-tools.d.ts +24 -0
- package/dist/fn-context/rpc/case-orchestrator-tools.js +13 -0
- package/dist/fn-context/rpc/index.d.ts +3 -1
- package/dist/fn-context/rpc/index.js +3 -1
- package/dist/fn-context/rpc/integration/index.d.ts +4 -4
- package/dist/fn-context/rpc/integration/index.js +1 -1
- package/dist/fn-context/rpc/integration/list-surfaces.d.ts +21 -0
- package/dist/fn-context/rpc/integration/{list-bindings.js → list-surfaces.js} +4 -4
- package/dist/fn-context/rpc/platform.d.ts +2 -2
- package/dist/fn-context/rpc/platform.js +1 -1
- package/dist/fn-context/rpc/records.d.ts +78 -0
- package/dist/fn-context/rpc/records.js +26 -0
- package/dist/fn-context/rpc/trpc-client.d.ts +2 -2
- package/dist/fn-context/rpc/trpc-client.js +88 -7
- package/dist/fn-context/rpc/trpc-init.d.ts +2 -2
- package/dist/fn-context/rpc/trpc-router-factory.d.ts +49 -3
- package/dist/fn-context/rpc/trpc-router-factory.js +2 -0
- package/dist/fn-context/run-scope.d.ts +4 -0
- package/dist/fn-context/run-scope.js +9 -0
- package/dist/fn-context/types.d.ts +36 -3
- package/dist/index.d.ts +37 -46
- package/dist/index.js +33 -33
- package/dist/logger.d.ts +7 -0
- package/dist/logger.js +41 -0
- package/dist/platform.d.ts +76 -0
- package/dist/platform.js +56 -0
- package/dist/voice-service/actions.d.ts +71 -4
- package/dist/voice-service/types.d.ts +42 -5
- package/dist/worker.d.ts +10 -0
- package/dist/worker.js +10 -0
- package/package.json +38 -19
- package/dist/builder/compile/compile-agenda.d.ts +0 -15
- package/dist/builder/compile/compile-agenda.js +0 -64
- package/dist/builder/merge.d.ts +0 -4
- package/dist/builder/merge.js +0 -19
- package/dist/compute-host/read-only-cache.d.ts +0 -20
- package/dist/compute-host/read-only-cache.js +0 -61
- package/dist/core/context/db.d.ts +0 -69
- package/dist/core/context/db.js +0 -44
- package/dist/core/context/docusign.d.ts +0 -134
- package/dist/core/context/docusign.js +0 -108
- package/dist/core/context/filevine.d.ts +0 -323
- package/dist/core/context/filevine.js +0 -254
- package/dist/core/context/http.d.ts +0 -9
- package/dist/core/context/http.js +0 -4
- package/dist/core/context/hubspot.d.ts +0 -250
- package/dist/core/context/hubspot.js +0 -191
- package/dist/core/context/twilio.d.ts +0 -50
- package/dist/core/context/twilio.js +0 -42
- package/dist/core/functions/define/case-context.d.ts +0 -14
- package/dist/core/functions/define/case-context.js +0 -15
- package/dist/core/functions/define/compute.d.ts +0 -7
- package/dist/core/functions/define/compute.js +0 -17
- package/dist/core/functions/define/custom.d.ts +0 -4
- package/dist/core/functions/define/custom.js +0 -8
- package/dist/core/functions/define/system.d.ts +0 -7
- package/dist/core/functions/define/system.js +0 -12
- package/dist/core/slot/deps.d.ts +0 -15
- package/dist/core/slot/deps.js +0 -88
- package/dist/core/slot/derived.d.ts +0 -32
- package/dist/core/slot/derived.js +0 -115
- package/dist/core/slot/platform-session-fields.d.ts +0 -38
- package/dist/core/slot/platform-session-fields.js +0 -48
- package/dist/core/slot/sf-schema.d.ts +0 -23
- package/dist/core/slot/sf-schema.js +0 -125
- package/dist/core/slot/sf-validate.d.ts +0 -14
- package/dist/core/slot/sf-validate.js +0 -132
- package/dist/core/slot/snapshot.d.ts +0 -18
- package/dist/core/slot/snapshot.js +0 -138
- package/dist/core/slot/write.d.ts +0 -48
- package/dist/core/slot/write.js +0 -426
- package/dist/core/stage/builders.d.ts +0 -54
- package/dist/core/stage/builders.js +0 -31
- package/dist/core/stage/condition.d.ts +0 -17
- package/dist/core/stage/condition.js +0 -198
- package/dist/core/stage/drive.d.ts +0 -43
- package/dist/core/stage/drive.js +0 -78
- package/dist/core/stage/finish-when.d.ts +0 -20
- package/dist/core/stage/finish-when.js +0 -82
- package/dist/core/stage/flatten.d.ts +0 -27
- package/dist/core/stage/flatten.js +0 -466
- package/dist/core/stage/index.d.ts +0 -15
- package/dist/core/stage/index.js +0 -13
- package/dist/core/stage/interrupts.d.ts +0 -28
- package/dist/core/stage/interrupts.js +0 -120
- package/dist/core/stage/ivr-fields.d.ts +0 -9
- package/dist/core/stage/ivr-fields.js +0 -75
- package/dist/core/stage/simulate.d.ts +0 -24
- package/dist/core/stage/simulate.js +0 -33
- package/dist/core/stage/transfer-fields.d.ts +0 -19
- package/dist/core/stage/transfer-fields.js +0 -129
- package/dist/core/stage/transfer-spec.d.ts +0 -48
- package/dist/core/stage/transfer-spec.js +0 -113
- package/dist/core/stage/types.d.ts +0 -208
- package/dist/core/stage/types.js +0 -2
- package/dist/core/stage/validate.d.ts +0 -11
- package/dist/core/stage/validate.js +0 -330
- package/dist/core/stage/walk.d.ts +0 -39
- package/dist/core/stage/walk.js +0 -119
- package/dist/fn-context/rpc/integration/list-bindings.d.ts +0 -21
- package/dist/fn-context/system-args.d.ts +0 -6
- package/dist/fn-context/system-args.js +0 -14
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
import { nameProblem } from './naming.js';
|
|
2
|
+
import { formatFieldPath } from './path.js';
|
|
3
|
+
import { PLATFORM_CONTACT_FIELDS, PLATFORM_SESSION_FIELDS } from './platform-fields.js';
|
|
4
|
+
import { isReservedTableName } from './row.js';
|
|
5
|
+
import { OPTION_DATA_TYPES, REMOVED_ENUM_KEY, REMOVED_IDENTIFIER_KEY, REMOVED_NULLABLE_KEY, REMOVED_SOURCE_KEY, ROW_ID_FIELD, SCALAR_DATA_TYPES, sessionSchemaSchema } from './schema.js';
|
|
6
|
+
export { PLATFORM_CONTACT_FIELDS, PLATFORM_SESSION_FIELDS } from './platform-fields.js';
|
|
7
|
+
/** The one table every company has: the only thing the platform contacts (14-sequence-instance §5). */
|
|
8
|
+
export const CONTACT_TABLE = 'contact';
|
|
9
|
+
/** The conversation's table: its fields are the company's `schema/session.ts`; no company table has this name. */
|
|
10
|
+
export const SESSION_TABLE = 'session';
|
|
11
|
+
export const CONTACT_OPT_OUT_FIELDS = ['opted_out', 'call_opted_out', 'sms_opted_out', 'email_opted_out'];
|
|
12
|
+
/** Platform limits (14-sequence-instance §16.1). */
|
|
13
|
+
export const LIMITS = {
|
|
14
|
+
/** Longest parent chain: a table's parent's parent … at most this many hops to a table without one. */
|
|
15
|
+
parentDepth: 5,
|
|
16
|
+
/** Rows one load step (one table) may read. */
|
|
17
|
+
loadRowsPerStep: 500,
|
|
18
|
+
/** `follow` hops chained in one load expression. */
|
|
19
|
+
followDepth: 3,
|
|
20
|
+
/** A load's time budget, ms. */
|
|
21
|
+
loadTimeoutMs: 2000,
|
|
22
|
+
/** An instance's `variables`, serialized, bytes. */
|
|
23
|
+
variablesBytes: 64 * 1024,
|
|
24
|
+
/** Company tables per company and environment (18-record-search-index §4.7). */
|
|
25
|
+
tables: 100,
|
|
26
|
+
/** Fields per table, platform fields included. */
|
|
27
|
+
fieldsPerTable: 300,
|
|
28
|
+
/** `options` of one `select` / `multi_select` field (a longer list is a table and a lookupRef). */
|
|
29
|
+
options: 200,
|
|
30
|
+
/** Characters of one option: a short code, not a sentence (its meaning goes in `extraction`). */
|
|
31
|
+
optionChars: 60,
|
|
32
|
+
/** A `string` value, characters. */
|
|
33
|
+
stringChars: 255,
|
|
34
|
+
/** A `text` value, UTF-8 bytes. */
|
|
35
|
+
textBytes: 64 * 1024,
|
|
36
|
+
/** One row's stored data, serialized, UTF-8 bytes. */
|
|
37
|
+
rowDataBytes: 256 * 1024,
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* The policy ids a run can name, per layer: the values of the platform's run variables
|
|
41
|
+
* `responsePolicy`, `slotFillingPolicy` and `voice` (14 §4.4). Customers write them, so an id is only
|
|
42
|
+
* ever added; removing or renaming one breaks the companies that use it.
|
|
43
|
+
*/
|
|
44
|
+
export const POLICY_IDS = {
|
|
45
|
+
response: ['default'],
|
|
46
|
+
slotFilling: ['default'],
|
|
47
|
+
voice: ['default'],
|
|
48
|
+
};
|
|
49
|
+
/** Session fields with the platform's only: what a company without fields of its own declares. */
|
|
50
|
+
export const PLATFORM_SESSION_SCHEMA = { fields: { ...PLATFORM_SESSION_FIELDS } };
|
|
51
|
+
/** Each table's platform fields, with the name its issue codes use (`missing-platform-<name>-field`). */
|
|
52
|
+
export const PLATFORM_TABLE_FIELDS = {
|
|
53
|
+
contact: { name: 'contact', fields: PLATFORM_CONTACT_FIELDS },
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* A company table other than `contact`: one the company defines whole. Every company table (contact
|
|
57
|
+
* included) is stored as `Record` rows; `session` is the sequence's, stored on the session.
|
|
58
|
+
*/
|
|
59
|
+
export function isCustomTable(tableName) {
|
|
60
|
+
return tableName !== CONTACT_TABLE && tableName !== SESSION_TABLE && !isReservedTableName(tableName);
|
|
61
|
+
}
|
|
62
|
+
/** A table whose rows are `Record` rows: every company table, contact included. */
|
|
63
|
+
export function isRecordTable(tableName) {
|
|
64
|
+
return tableName !== SESSION_TABLE && !isReservedTableName(tableName);
|
|
65
|
+
}
|
|
66
|
+
/** A table's parent: the field name and the table it points at; null when it has none. */
|
|
67
|
+
export function parentOf(table) {
|
|
68
|
+
if (!table)
|
|
69
|
+
return null;
|
|
70
|
+
for (const [field, def] of Object.entries(table.fields)) {
|
|
71
|
+
const ref = def.ref;
|
|
72
|
+
if (ref?.kind === 'parent')
|
|
73
|
+
return { field, table: ref.table };
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Whether `fieldName` is the table's parent. Its value is the row's owner: filled by the platform or
|
|
79
|
+
* given once on insert, never written after, and stored as the row's parent edge in `RecordRef`, not
|
|
80
|
+
* in its data.
|
|
81
|
+
*/
|
|
82
|
+
export function isParentField(table, fieldName) {
|
|
83
|
+
return parentOf(table)?.field === fieldName;
|
|
84
|
+
}
|
|
85
|
+
/** A field as stored: a copy of it. */
|
|
86
|
+
function storedField(field) {
|
|
87
|
+
return structuredClone(field);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The company schema as authored, without the reserved names (as `compileSlotSchema` leaves them
|
|
91
|
+
* out). It is plain data.
|
|
92
|
+
*/
|
|
93
|
+
export function compileCompanySchema(company) {
|
|
94
|
+
const compiled = {};
|
|
95
|
+
for (const [tableName, table] of Object.entries(company)) {
|
|
96
|
+
if (tableName === SESSION_TABLE || isReservedTableName(tableName))
|
|
97
|
+
continue;
|
|
98
|
+
compiled[tableName] = {
|
|
99
|
+
...structuredClone(table),
|
|
100
|
+
fields: Object.fromEntries(Object.entries(table.fields).map(([name, field]) => [name, storedField(field)])),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return compiled;
|
|
104
|
+
}
|
|
105
|
+
/** The session schema as stored. */
|
|
106
|
+
function compileSessionSchema(session) {
|
|
107
|
+
return { fields: Object.fromEntries(Object.entries(session.fields).map(([name, field]) => [name, storedField(field)])) };
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Why `schema/session.ts` is not a session schema (11-remove-sequence-schema §3.1): its fields are
|
|
111
|
+
* simple values, with no `ref` (the session is related to no other table).
|
|
112
|
+
*/
|
|
113
|
+
export function sessionSchemaIssues(session) {
|
|
114
|
+
const issues = [];
|
|
115
|
+
const fields = session.fields;
|
|
116
|
+
if (fields && typeof fields === 'object') {
|
|
117
|
+
for (const [fieldName, field] of Object.entries(fields)) {
|
|
118
|
+
const path = formatFieldPath(SESSION_TABLE, fieldName);
|
|
119
|
+
if (fieldName === ROW_ID_FIELD) {
|
|
120
|
+
issues.push({ code: 'invalid-session-field', message: `schema/session.ts: ${path}: the name is reserved`, path });
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (!field || typeof field !== 'object')
|
|
124
|
+
continue;
|
|
125
|
+
if (Object.hasOwn(field, REMOVED_SOURCE_KEY))
|
|
126
|
+
issues.push(sourceRemovedIssue(path, 'schema/session.ts: '));
|
|
127
|
+
if (Object.hasOwn(field, REMOVED_NULLABLE_KEY))
|
|
128
|
+
issues.push(nullableRemovedIssue(path, 'schema/session.ts: '));
|
|
129
|
+
if (Object.hasOwn(field, REMOVED_IDENTIFIER_KEY))
|
|
130
|
+
issues.push(identifierRemovedIssue(path, 'schema/session.ts: '));
|
|
131
|
+
issues.push(...optionIssues(path, field, 'schema/session.ts: '));
|
|
132
|
+
if ('unique' in field) {
|
|
133
|
+
issues.push({ code: 'invalid-session-field', message: `schema/session.ts: ${path}: a session field cannot be unique (there is one session row per conversation)`, path });
|
|
134
|
+
}
|
|
135
|
+
if ('ref' in field) {
|
|
136
|
+
issues.push({
|
|
137
|
+
code: 'invalid-session-field',
|
|
138
|
+
message: `schema/session.ts: ${path}: a session field has no ref (the session is related to no other table)`,
|
|
139
|
+
path,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
if (field.dataType === 'contact_schedule') {
|
|
143
|
+
issues.push({
|
|
144
|
+
code: 'invalid-session-field',
|
|
145
|
+
message: `schema/session.ts: ${path}: a session field is a simple value (${SCALAR_DATA_TYPES.join(', ')})`,
|
|
146
|
+
path,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (issues.length > 0)
|
|
152
|
+
return issues;
|
|
153
|
+
for (const issue of sessionSchemaSchema.safeParse(session).error?.issues ?? []) {
|
|
154
|
+
const [, fieldName, ...rest] = issue.path.map(String);
|
|
155
|
+
const path = fieldName ? formatFieldPath(SESSION_TABLE, fieldName) : SESSION_TABLE;
|
|
156
|
+
issues.push({
|
|
157
|
+
code: 'invalid-session-field',
|
|
158
|
+
message: `schema/session.ts: ${[path, ...rest].join('.')}: ${issue.message}`,
|
|
159
|
+
path,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return issues;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* A ref's `relatedName` (13-data-model-v1 §23.4 items 3 and 6): a name like any other, and one the
|
|
166
|
+
* target table does not already use: not one of its fields, not another ref's `relatedName` to it.
|
|
167
|
+
* The way back is a key of the target row, beside its fields.
|
|
168
|
+
*/
|
|
169
|
+
function relatedNameIssues(company) {
|
|
170
|
+
const issues = [];
|
|
171
|
+
const taken = new Map();
|
|
172
|
+
for (const [tableName, table] of Object.entries(company)) {
|
|
173
|
+
if (tableName === SESSION_TABLE || isReservedTableName(tableName))
|
|
174
|
+
continue;
|
|
175
|
+
for (const [fieldName, field] of Object.entries(table.fields ?? {})) {
|
|
176
|
+
const name = field?.ref?.relatedName;
|
|
177
|
+
if (name === undefined)
|
|
178
|
+
continue;
|
|
179
|
+
const path = formatFieldPath(tableName, fieldName);
|
|
180
|
+
const target = field.ref.table;
|
|
181
|
+
const problem = typeof name === 'string' ? nameProblem(name) : { code: 'invalid-name', reason: 'must be a string' };
|
|
182
|
+
if (problem) {
|
|
183
|
+
issues.push({ code: problem.code, message: `${path}: relatedName ${JSON.stringify(name)} is not a relation name: ${problem.reason}`, path });
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const targetTable = Object.hasOwn(company, target) ? company[target] : undefined;
|
|
187
|
+
// The row id is the platform's, on every table: a field of it all the same.
|
|
188
|
+
if (targetTable && (name === ROW_ID_FIELD || Object.hasOwn(targetTable.fields ?? {}, name))) {
|
|
189
|
+
issues.push({ code: 'related-name-conflict', message: `${path}: relatedName ${name} is already a field of ${target}`, path });
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const names = taken.get(target) ?? new Map();
|
|
193
|
+
const other = names.get(name);
|
|
194
|
+
if (other) {
|
|
195
|
+
issues.push({ code: 'related-name-conflict', message: `${path}: relatedName ${name} is already ${other}'s way back to ${target}`, path });
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
names.set(name, path);
|
|
199
|
+
taken.set(target, names);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return issues;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Names the schema may not use (13-data-model-v1 §23.4 item 6): every table name, field name and
|
|
206
|
+
* session field name is checked once here, for the whole company, not per sequence.
|
|
207
|
+
*/
|
|
208
|
+
function schemaNameIssues(company, session) {
|
|
209
|
+
const issues = [];
|
|
210
|
+
const check = (name, path, what) => {
|
|
211
|
+
const problem = nameProblem(name);
|
|
212
|
+
if (problem)
|
|
213
|
+
issues.push({ code: problem.code, message: `${path}: not a ${what} name: ${problem.reason}`, path });
|
|
214
|
+
};
|
|
215
|
+
for (const [tableName, table] of Object.entries(company)) {
|
|
216
|
+
if (tableName === SESSION_TABLE || isReservedTableName(tableName))
|
|
217
|
+
continue;
|
|
218
|
+
check(tableName, tableName, 'table');
|
|
219
|
+
for (const [fieldName, field] of Object.entries(table.fields ?? {})) {
|
|
220
|
+
const path = formatFieldPath(tableName, fieldName);
|
|
221
|
+
check(fieldName, path, 'field');
|
|
222
|
+
// A ref is the related row, its `.value` the id: named for the row (`injury`), not `injury_id` (§23.4 item 1).
|
|
223
|
+
if (field?.ref && fieldName.endsWith('_id')) {
|
|
224
|
+
const noun = fieldName.slice(0, -'_id'.length);
|
|
225
|
+
issues.push({
|
|
226
|
+
code: 'ref-name-id-suffix',
|
|
227
|
+
message: `${path}: a ref is named for the row it points at, without _id (e.g. ${noun}: ${field.ref.kind}Ref('${field.ref.table}'))`,
|
|
228
|
+
path,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
issues.push(...relatedNameIssues(company));
|
|
234
|
+
const sessionFields = session.fields;
|
|
235
|
+
if (sessionFields && typeof sessionFields === 'object') {
|
|
236
|
+
for (const fieldName of Object.keys(sessionFields)) {
|
|
237
|
+
if (fieldName === ROW_ID_FIELD)
|
|
238
|
+
continue; // `invalid-session-field` reports it
|
|
239
|
+
check(fieldName, formatFieldPath(SESSION_TABLE, fieldName), 'session field');
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return issues;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* The company's metadata: its tables (`schema/index.ts`) and its session fields (`schema/session.ts`,
|
|
246
|
+
* none when the file is missing). `session` is reserved: no company table has the name.
|
|
247
|
+
*/
|
|
248
|
+
export function compileCompanyMetadata(company, session = PLATFORM_SESSION_SCHEMA) {
|
|
249
|
+
const issues = [];
|
|
250
|
+
if (Object.hasOwn(company, SESSION_TABLE)) {
|
|
251
|
+
issues.push({
|
|
252
|
+
code: 'reserved-table-name',
|
|
253
|
+
message: "Table name is reserved: session is the conversation's; define its fields in schema/session.ts",
|
|
254
|
+
path: SESSION_TABLE,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
for (const tableName of Object.keys(company)) {
|
|
258
|
+
if (!isReservedTableName(tableName))
|
|
259
|
+
continue;
|
|
260
|
+
issues.push({ code: 'reserved-table-name', message: `Table name is reserved: ${tableName}`, path: tableName });
|
|
261
|
+
}
|
|
262
|
+
issues.push(...sessionSchemaIssues(session));
|
|
263
|
+
issues.push(...schemaNameIssues(company, session));
|
|
264
|
+
if (!Object.hasOwn(company, CONTACT_TABLE)) {
|
|
265
|
+
issues.push({
|
|
266
|
+
code: 'missing-contact-table',
|
|
267
|
+
message: 'the company schema must define the contact table (spread PLATFORM_CONTACT_FIELDS into it)',
|
|
268
|
+
path: CONTACT_TABLE,
|
|
269
|
+
});
|
|
270
|
+
return { metadata: null, validation: { ok: false, issues } };
|
|
271
|
+
}
|
|
272
|
+
validatePlatformFields(company, CONTACT_TABLE, issues);
|
|
273
|
+
validatePlatformSessionFields(session, issues);
|
|
274
|
+
validateRelationships(company, issues);
|
|
275
|
+
issues.push(...fieldRuleIssues(company));
|
|
276
|
+
const metadata = issues.length === 0 ? { tables: compileCompanySchema(company), session: compileSessionSchema(session) } : null;
|
|
277
|
+
return { metadata, validation: issues.length === 0 ? { ok: true } : { ok: false, issues } };
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Relationship rules (14-sequence-instance §16.1): a table has at most one parent, a required system
|
|
281
|
+
* ref to another company table (not session, not itself); the parents form no cycle and no chain
|
|
282
|
+
* longer than {@link LIMITS.parentDepth}; `contact` has no parent (it belongs to no one thing). A
|
|
283
|
+
* ref no longer takes `unique` (13-data-model-v1 §22.4): a schema written before says so instead of
|
|
284
|
+
* silently getting a list.
|
|
285
|
+
*/
|
|
286
|
+
function validateRelationships(company, issues) {
|
|
287
|
+
const parentTargets = new Map();
|
|
288
|
+
for (const [tableName, table] of Object.entries(company)) {
|
|
289
|
+
if (tableName === SESSION_TABLE || isReservedTableName(tableName))
|
|
290
|
+
continue;
|
|
291
|
+
const parents = [];
|
|
292
|
+
for (const [fieldName, field] of Object.entries(table.fields)) {
|
|
293
|
+
if (!field.ref)
|
|
294
|
+
continue;
|
|
295
|
+
const path = formatFieldPath(tableName, fieldName);
|
|
296
|
+
const { ref } = field;
|
|
297
|
+
if ('unique' in ref) {
|
|
298
|
+
issues.push({
|
|
299
|
+
code: 'unique-removed',
|
|
300
|
+
message: `${path}: refs no longer take unique; a parent row has any number of rows under it (fields of one per parent belong on the parent table)`,
|
|
301
|
+
path,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
if (ref.kind !== 'parent')
|
|
305
|
+
continue;
|
|
306
|
+
parents.push(fieldName);
|
|
307
|
+
if (tableName === CONTACT_TABLE) {
|
|
308
|
+
issues.push({
|
|
309
|
+
code: 'contact-parent-forbidden',
|
|
310
|
+
message: `${path}: a contact belongs to no one row, so it has no parent; use lookupRef('${ref.table}') instead`,
|
|
311
|
+
path,
|
|
312
|
+
});
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
if (ref.table === SESSION_TABLE || ref.table === tableName) {
|
|
316
|
+
issues.push({
|
|
317
|
+
code: 'parent-target-invalid',
|
|
318
|
+
message: `${path}: a parent must point at another company table, got ${ref.table}`,
|
|
319
|
+
path,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
parentTargets.set(tableName, ref.table);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (parents.length > 1) {
|
|
327
|
+
issues.push({
|
|
328
|
+
code: 'multiple-parents',
|
|
329
|
+
message: `${tableName} has more than one parent (${parents.join(', ')}); keep one and make the others lookups (lookupRef)`,
|
|
330
|
+
path: tableName,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
for (const tableName of parentTargets.keys()) {
|
|
335
|
+
const seen = [tableName];
|
|
336
|
+
let next = parentTargets.get(tableName);
|
|
337
|
+
while (next !== undefined) {
|
|
338
|
+
if (seen.includes(next)) {
|
|
339
|
+
issues.push({
|
|
340
|
+
code: 'parent-cycle',
|
|
341
|
+
message: `${tableName}: its parents form a cycle (${[...seen, next].join(' → ')})`,
|
|
342
|
+
path: tableName,
|
|
343
|
+
});
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
seen.push(next);
|
|
347
|
+
next = parentTargets.get(next);
|
|
348
|
+
}
|
|
349
|
+
if (next === undefined && seen.length - 1 > LIMITS.parentDepth) {
|
|
350
|
+
issues.push({
|
|
351
|
+
code: 'parent-chain-too-long',
|
|
352
|
+
message: `${tableName}: its parent chain has ${seen.length - 1} levels (${seen.join(' → ')}); at most ${LIMITS.parentDepth}`,
|
|
353
|
+
path: tableName,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/** The issue for a field that still names its `source` (19-page-writable-cells). */
|
|
359
|
+
export function sourceRemovedIssue(path, prefix = '') {
|
|
360
|
+
return {
|
|
361
|
+
code: 'source-removed',
|
|
362
|
+
message: `${prefix}${path}: remove source; a page decides what the AI may write (field(...) opens a cell, anything else on the page is read-only)`,
|
|
363
|
+
path,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
/** The issue for a field that still names `nullable`, which no write checked. */
|
|
367
|
+
export function nullableRemovedIssue(path, prefix = '') {
|
|
368
|
+
return {
|
|
369
|
+
code: 'nullable-removed',
|
|
370
|
+
message: `${prefix}${path}: remove nullable; every field starts empty and can be cleared (a page's required says when one must be filled)`,
|
|
371
|
+
path,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
/** The issue for a field that still names `identifier`, which nothing used. */
|
|
375
|
+
export function identifierRemovedIssue(path, prefix = '') {
|
|
376
|
+
return {
|
|
377
|
+
code: 'identifier-removed',
|
|
378
|
+
message: `${prefix}${path}: remove identifier; resolve.ts decides who a conversation is with (ctx.api.contact.find looks up phone and email)`,
|
|
379
|
+
path,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* A field's choices (27 §2–§3): no `enum` (now a `select`); `options` on a `select` / `multi_select`
|
|
384
|
+
* only, and there required: 1 to `LIMITS.options` distinct short codes.
|
|
385
|
+
*/
|
|
386
|
+
export function optionIssues(path, field, prefix = '') {
|
|
387
|
+
const issues = [];
|
|
388
|
+
if (Object.hasOwn(field, REMOVED_ENUM_KEY)) {
|
|
389
|
+
issues.push({
|
|
390
|
+
code: 'enum-removed',
|
|
391
|
+
message: `${prefix}${path}: enum is removed; a field of a known set of values is dataType 'select' (one of them) or 'multi_select' (any of them) with options: [...]`,
|
|
392
|
+
path,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
const options = field.options;
|
|
396
|
+
const takes = OPTION_DATA_TYPES.has(field.dataType);
|
|
397
|
+
if (!takes) {
|
|
398
|
+
if (options !== undefined) {
|
|
399
|
+
issues.push({ code: 'options-on-wrong-type', message: `${prefix}${path}: options are on a select or multi_select field only (this one is ${String(field.dataType)})`, path });
|
|
400
|
+
}
|
|
401
|
+
return issues;
|
|
402
|
+
}
|
|
403
|
+
if (!Array.isArray(options) || options.length === 0) {
|
|
404
|
+
issues.push({ code: 'missing-options', message: `${prefix}${path}: a ${String(field.dataType)} field needs options: [...] (at least one)`, path });
|
|
405
|
+
return issues;
|
|
406
|
+
}
|
|
407
|
+
if (options.length > LIMITS.options) {
|
|
408
|
+
issues.push({ code: 'too-many-options', message: `${prefix}${path}: ${options.length} options; at most ${LIMITS.options} (make it a table and a lookupRef instead)`, path });
|
|
409
|
+
}
|
|
410
|
+
const bad = options.filter((v) => typeof v !== 'string' || v.length === 0);
|
|
411
|
+
if (bad.length) {
|
|
412
|
+
issues.push({ code: 'invalid-option', message: `${prefix}${path}: options are non-empty strings (${bad.map((v) => JSON.stringify(v)).join(', ')})`, path });
|
|
413
|
+
}
|
|
414
|
+
const long = options.filter((v) => typeof v === 'string' && v.length > LIMITS.optionChars);
|
|
415
|
+
if (long.length) {
|
|
416
|
+
issues.push({
|
|
417
|
+
code: 'option-too-long',
|
|
418
|
+
message: `${prefix}${path}: options are short codes, at most ${LIMITS.optionChars} characters (${long.map((v) => JSON.stringify(v.slice(0, 30) + '…')).join(', ')}); say what each means in extraction`,
|
|
419
|
+
path,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
const seen = new Set();
|
|
423
|
+
const repeated = options.filter((v) => (seen.has(v) ? true : (seen.add(v), false)));
|
|
424
|
+
if (repeated.length) {
|
|
425
|
+
issues.push({ code: 'duplicate-option', message: `${prefix}${path}: options repeat ${[...new Set(repeated)].map((v) => JSON.stringify(v)).join(', ')}`, path });
|
|
426
|
+
}
|
|
427
|
+
return issues;
|
|
428
|
+
}
|
|
429
|
+
/** The issue for a table that writes the row id, which is the platform's. */
|
|
430
|
+
export function idIsPlatformIssue(path) {
|
|
431
|
+
return {
|
|
432
|
+
code: 'id-is-platform',
|
|
433
|
+
message: `${path}: remove id; every table has it (with createdAt and updatedAt), the platform's`,
|
|
434
|
+
path,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
/** Types a `unique` field may have: values compared as one stored scalar (18-record-search-index §4.5). */
|
|
438
|
+
const UNIQUE_DATA_TYPES = new Set(['string', 'phone', 'email', 'number']);
|
|
439
|
+
/**
|
|
440
|
+
* Field and size rules (18-record-search-index §4.2, §4.5, §4.7): no `id` (the platform's), no
|
|
441
|
+
* `source`, `nullable`, `identifier` or `enum`; the table and field limits, a field's options; `unique` only on a plain
|
|
442
|
+
* value of a type compared as stored.
|
|
443
|
+
*/
|
|
444
|
+
function fieldRuleIssues(company) {
|
|
445
|
+
const issues = [];
|
|
446
|
+
const tables = Object.entries(company).filter(([tableName]) => tableName !== SESSION_TABLE && !isReservedTableName(tableName));
|
|
447
|
+
if (tables.length > LIMITS.tables) {
|
|
448
|
+
issues.push({ code: 'too-many-tables', message: `the company schema has ${tables.length} tables; at most ${LIMITS.tables}`, path: '' });
|
|
449
|
+
}
|
|
450
|
+
for (const [tableName, table] of tables) {
|
|
451
|
+
const fields = Object.entries(table.fields ?? {});
|
|
452
|
+
// The platform's row id counts too.
|
|
453
|
+
const count = fields.filter(([name]) => name !== ROW_ID_FIELD).length + 1;
|
|
454
|
+
if (count > LIMITS.fieldsPerTable) {
|
|
455
|
+
issues.push({ code: 'too-many-fields', message: `${tableName} has ${count} fields with its id; at most ${LIMITS.fieldsPerTable}`, path: tableName });
|
|
456
|
+
}
|
|
457
|
+
for (const [fieldName, field] of fields) {
|
|
458
|
+
if (!field || typeof field !== 'object')
|
|
459
|
+
continue;
|
|
460
|
+
const path = formatFieldPath(tableName, fieldName);
|
|
461
|
+
if (fieldName === ROW_ID_FIELD) {
|
|
462
|
+
issues.push(idIsPlatformIssue(path));
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
if (Object.hasOwn(field, REMOVED_SOURCE_KEY))
|
|
466
|
+
issues.push(sourceRemovedIssue(path));
|
|
467
|
+
if (Object.hasOwn(field, REMOVED_NULLABLE_KEY))
|
|
468
|
+
issues.push(nullableRemovedIssue(path));
|
|
469
|
+
if (Object.hasOwn(field, REMOVED_IDENTIFIER_KEY))
|
|
470
|
+
issues.push(identifierRemovedIssue(path));
|
|
471
|
+
issues.push(...optionIssues(path, field));
|
|
472
|
+
if (field.unique !== undefined) {
|
|
473
|
+
if (field.unique !== true || !UNIQUE_DATA_TYPES.has(field.dataType) || field.ref) {
|
|
474
|
+
issues.push({
|
|
475
|
+
code: 'invalid-unique',
|
|
476
|
+
message: `${path}: unique is true or absent, on a string, phone, email or number field that is not a ref`,
|
|
477
|
+
path,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return issues;
|
|
484
|
+
}
|
|
485
|
+
/** Every platform field of `tableName` is in the company schema, defined as the runtime defines it. */
|
|
486
|
+
function validatePlatformFields(company, tableName, issues) {
|
|
487
|
+
const platform = PLATFORM_TABLE_FIELDS[tableName];
|
|
488
|
+
for (const [name, required] of Object.entries(platform.fields)) {
|
|
489
|
+
const path = `${tableName}.${name}`;
|
|
490
|
+
const actual = company[tableName]?.fields[name];
|
|
491
|
+
if (!actual) {
|
|
492
|
+
issues.push({ code: `missing-platform-${platform.name}-field`, message: `${path} is required`, path });
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
const differing = differingProperties(actual, required);
|
|
496
|
+
if (differing.length > 0) {
|
|
497
|
+
issues.push({
|
|
498
|
+
code: `invalid-platform-${platform.name}-field`,
|
|
499
|
+
message: `${path} must match the runtime field definition; it differs in: ${differing.join(', ')}`,
|
|
500
|
+
path,
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
/** Every platform session field is in `schema/session.ts`, defined as the runtime defines it (24 §5.2). */
|
|
506
|
+
function validatePlatformSessionFields(session, issues) {
|
|
507
|
+
for (const [name, required] of Object.entries(PLATFORM_SESSION_FIELDS)) {
|
|
508
|
+
const path = `${SESSION_TABLE}.${name}`;
|
|
509
|
+
const actual = session.fields?.[name];
|
|
510
|
+
if (!actual) {
|
|
511
|
+
issues.push({ code: 'missing-platform-session-field', message: `${path} is required: spread PLATFORM_SESSION_FIELDS into schema/session.ts`, path });
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
const differing = differingProperties(actual, required);
|
|
515
|
+
if (differing.length > 0) {
|
|
516
|
+
issues.push({ code: 'invalid-platform-session-field', message: `${path} must match the runtime field definition; it differs in: ${differing.join(', ')}`, path });
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
/** The company table a sequence names as its business root, checked (14 §4.5). */
|
|
521
|
+
export function subjectTableIssues(company, subject) {
|
|
522
|
+
if (typeof subject !== 'string' || subject.length === 0) {
|
|
523
|
+
return [
|
|
524
|
+
{
|
|
525
|
+
code: 'missing-subject',
|
|
526
|
+
message: "the sequence must declare its business root: export default defineSequence({ subject: '<table>' }) in index.ts",
|
|
527
|
+
path: 'subject',
|
|
528
|
+
},
|
|
529
|
+
];
|
|
530
|
+
}
|
|
531
|
+
if (subject === SESSION_TABLE || isReservedTableName(subject) || !Object.hasOwn(company, subject)) {
|
|
532
|
+
return [{ code: 'unknown-subject', message: `subject is not a company table: ${subject}`, path: 'subject' }];
|
|
533
|
+
}
|
|
534
|
+
return [];
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* The properties where an authored required field departs from its runtime definition, by value:
|
|
538
|
+
* key order never matters, and an `undefined` property or `deprecated: false` reads as absent
|
|
539
|
+
* (both mean the default). `deprecated: true` is a difference: a required field cannot be retired.
|
|
540
|
+
*/
|
|
541
|
+
function differingProperties(actual, required) {
|
|
542
|
+
const authored = presentProperties(actual);
|
|
543
|
+
const canonical = presentProperties(required);
|
|
544
|
+
const keys = [...new Set([...Object.keys(authored), ...Object.keys(canonical)])].sort();
|
|
545
|
+
return keys.filter((key) => !sameValue(authored[key], canonical[key]));
|
|
546
|
+
}
|
|
547
|
+
function presentProperties(field) {
|
|
548
|
+
return Object.fromEntries(Object.entries(field).filter(([key, value]) => value !== undefined && !(key === 'deprecated' && value === false)));
|
|
549
|
+
}
|
|
550
|
+
/** Deep equality of plain JSON values, ignoring object key order and `undefined` properties. */
|
|
551
|
+
function sameValue(a, b) {
|
|
552
|
+
if (a === b)
|
|
553
|
+
return true;
|
|
554
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
555
|
+
return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((item, i) => sameValue(item, b[i]));
|
|
556
|
+
}
|
|
557
|
+
if (!a || !b || typeof a !== 'object' || typeof b !== 'object')
|
|
558
|
+
return false;
|
|
559
|
+
const left = Object.entries(a).filter(([, value]) => value !== undefined);
|
|
560
|
+
const right = new Map(Object.entries(b).filter(([, value]) => value !== undefined));
|
|
561
|
+
return left.length === right.size && left.every(([key, value]) => right.has(key) && sameValue(value, right.get(key)));
|
|
562
|
+
}
|
|
563
|
+
//# sourceMappingURL=company-metadata.js.map
|
|
@@ -1,24 +1,30 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
import { type FieldPath } from './path.js';
|
|
1
|
+
import type { CompanyFieldDef, CompanySchema, SessionSchema } from './schema.js';
|
|
2
|
+
import type { CompiledSlotSchema, RuntimeFieldDef, RuntimeTableDef, ValidationResult } from './types.js';
|
|
3
|
+
export declare function companyFieldToRuntime(field: CompanyFieldDef): RuntimeFieldDef;
|
|
5
4
|
/**
|
|
6
|
-
*
|
|
5
|
+
* The company schema as the platform reads it (28 §2): every company table and field, and the
|
|
6
|
+
* company's session fields (`schema/session.ts`). One for the whole company: every sequence, page and
|
|
7
|
+
* function reads it; a sequence only names its `subject` (checked by `subjectTableIssues`).
|
|
7
8
|
*/
|
|
8
|
-
export declare function compileSlotSchema(company: CompanySchema,
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
export declare function compileSlotSchema(company: CompanySchema, options?: {
|
|
10
|
+
/** The company's session fields (`schema/session.ts`); none when omitted. */
|
|
11
|
+
session?: SessionSchema;
|
|
11
12
|
}): {
|
|
12
13
|
schema: CompiledSlotSchema;
|
|
13
14
|
validation: ValidationResult;
|
|
14
15
|
};
|
|
15
16
|
/** Validate field paths exist on a compiled schema. */
|
|
16
17
|
export declare function validateFieldRefs(schema: CompiledSlotSchema, refs: Iterable<string>): ValidationResult;
|
|
18
|
+
/** A way back to a table (13-data-model-v1 §23.4 item 3): the rows of `table` whose ref `field` points at it. */
|
|
19
|
+
export type TableRelation = {
|
|
20
|
+
name: string;
|
|
21
|
+
table: string;
|
|
22
|
+
field: string;
|
|
23
|
+
};
|
|
24
|
+
/** The ways back to `tableName`: every ref of this schema that points at it and has a `relatedName`. */
|
|
25
|
+
export declare function relationsOf(schema: CompiledSlotSchema, tableName: string): TableRelation[];
|
|
26
|
+
/** A table's own field by name; undefined for any other key, a name every object has (`toString`) included. */
|
|
27
|
+
export declare function getTableField(table: Pick<RuntimeTableDef, 'fields'>, fieldName: string): RuntimeFieldDef | undefined;
|
|
17
28
|
export declare function getCompiledTable(schema: CompiledSlotSchema, tableName: string): RuntimeTableDef | undefined;
|
|
18
29
|
export declare function compiledTableEntries(schema: CompiledSlotSchema): Array<[string, RuntimeTableDef]>;
|
|
19
|
-
/** Snapshot / SF presentation: single → field dict, multi → row array. */
|
|
20
|
-
export declare function isSnapshotSingleRow(table: RuntimeTableDef): boolean;
|
|
21
|
-
/** Effective schema for Slots codegen (multi → array, single → dict). */
|
|
22
|
-
export declare function toEffectiveSchema(compiled: CompiledSlotSchema): EffectiveSchema;
|
|
23
|
-
export declare function listConversationFields(schema: CompiledSlotSchema): FieldPath[];
|
|
24
30
|
//# sourceMappingURL=compile-schema.d.ts.map
|