@superpanel-sdk/runtime 0.2.3 → 0.7.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 +30 -10
- package/dist/builder/codegen/emit.d.ts +25 -12
- package/dist/builder/codegen/emit.js +339 -78
- package/dist/builder/codegen/generate.d.ts +21 -2
- package/dist/builder/codegen/generate.js +147 -34
- 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 +29 -0
- package/dist/builder/company-imports.js +122 -0
- package/dist/builder/compile/compile-sequence.d.ts +61 -8
- package/dist/builder/compile/compile-sequence.js +334 -80
- package/dist/builder/compile/function-code.d.ts +19 -3
- package/dist/builder/compile/function-code.js +123 -45
- package/dist/builder/compile/registry.d.ts +7 -3
- package/dist/builder/compile/registry.js +30 -27
- package/dist/builder/errors.d.ts +9 -0
- package/dist/builder/errors.js +17 -0
- package/dist/builder/function-params.d.ts +10 -0
- package/dist/builder/function-params.js +203 -0
- package/dist/builder/load.d.ts +70 -10
- package/dist/builder/load.js +258 -67
- package/dist/builder/paths.d.ts +9 -1
- package/dist/builder/paths.js +32 -2
- package/dist/builder/schedules.d.ts +25 -0
- package/dist/builder/schedules.js +101 -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 +102 -0
- package/dist/builder/types.d.ts +127 -19
- package/dist/builder/types.js +21 -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/calls/index.d.ts +177 -0
- package/dist/cli/calls/index.js +96 -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/debug/index.d.ts +31 -0
- package/dist/cli/debug/index.js +119 -0
- package/dist/cli/exec/index.d.ts +39 -0
- package/dist/cli/exec/index.js +138 -0
- package/dist/cli/exec/session.d.ts +48 -0
- package/dist/cli/exec/session.js +82 -0
- package/dist/cli/exec/trace.d.ts +18 -0
- package/dist/cli/exec/trace.js +52 -0
- package/dist/cli/index.js +112 -9
- package/dist/cli/logs/index.d.ts +47 -0
- package/dist/cli/logs/index.js +113 -0
- package/dist/cli/mobile/index.d.ts +12 -0
- package/dist/cli/mobile/index.js +405 -0
- package/dist/cli/serve/index.d.ts +109 -3
- package/dist/cli/serve/index.js +506 -118
- package/dist/cli/turns/index.d.ts +105 -0
- package/dist/cli/turns/index.js +128 -0
- package/dist/cli/turns/run.d.ts +13 -0
- package/dist/cli/turns/run.js +57 -0
- package/dist/cli/turns/shadow.d.ts +112 -0
- package/dist/cli/turns/shadow.js +54 -0
- package/dist/cli/type-gen/index.js +9 -9
- package/dist/compiler.d.ts +7 -0
- package/dist/compiler.js +7 -0
- package/dist/compute-host/compute-host.d.ts +14 -23
- package/dist/compute-host/compute-host.js +18 -30
- 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 +10 -10
- package/dist/compute-host/source-compute-host.js +33 -40
- package/dist/core/case-orchestrator/contact-hours.d.ts +70 -0
- package/dist/core/case-orchestrator/contact-hours.js +127 -0
- package/dist/core/case-orchestrator/index.d.ts +3 -0
- package/dist/core/case-orchestrator/index.js +3 -0
- package/dist/core/case-orchestrator/render.d.ts +75 -0
- package/dist/core/case-orchestrator/render.js +244 -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/types.d.ts +708 -0
- package/dist/core/case-orchestrator/types.js +42 -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 +198 -0
- package/dist/core/context/email-message.js +86 -0
- package/dist/core/context/email.d.ts +109 -0
- package/dist/core/context/email.js +90 -0
- package/dist/core/context/exchanges.d.ts +167 -0
- package/dist/core/context/exchanges.js +81 -0
- package/dist/core/context/instances.d.ts +307 -0
- package/dist/core/context/instances.js +137 -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 +544 -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 +177 -0
- package/dist/core/context/sms.js +77 -0
- package/dist/core/context/types.d.ts +79 -48
- package/dist/core/context/types.js +1 -7
- package/dist/core/conversation/index.d.ts +85 -0
- package/dist/core/conversation/index.js +133 -0
- package/dist/core/functions/errors.d.ts +74 -0
- package/dist/core/functions/errors.js +26 -0
- package/dist/core/functions/events.d.ts +192 -0
- package/dist/core/functions/events.js +23 -0
- package/dist/core/functions/schedules.d.ts +50 -0
- package/dist/core/functions/schedules.js +20 -0
- package/dist/core/functions/types.d.ts +29 -73
- package/dist/core/functions/types.js +1 -4
- 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 +904 -0
- package/dist/core/page/define.js +518 -0
- package/dist/core/page/envelope.d.ts +55 -0
- package/dist/core/page/envelope.js +4 -0
- package/dist/core/page/fns.d.ts +448 -0
- package/dist/core/page/fns.js +539 -0
- package/dist/core/page/memory-reader.d.ts +27 -0
- package/dist/core/page/memory-reader.js +299 -0
- package/dist/core/page/ops.d.ts +58 -0
- package/dist/core/page/ops.js +220 -0
- package/dist/core/page/page-host.d.ts +20 -0
- package/dist/core/page/page-host.js +57 -0
- package/dist/core/page/records-query.d.ts +234 -0
- package/dist/core/page/records-query.js +780 -0
- package/dist/core/page/render.d.ts +295 -0
- package/dist/core/page/render.js +1060 -0
- package/dist/core/page/sandbox.d.ts +85 -0
- package/dist/core/page/sandbox.js +357 -0
- package/dist/core/page/time-format.d.ts +46 -0
- package/dist/core/page/time-format.js +125 -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 +648 -0
- package/dist/core/page/typed.js +2 -0
- package/dist/core/page/workspace.d.ts +57 -0
- package/dist/core/page/workspace.js +266 -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 +208 -0
- package/dist/core/party/schema.js +113 -0
- package/dist/core/party/types.d.ts +35 -0
- package/dist/core/party/types.js +15 -0
- package/dist/core/sequence/define.d.ts +56 -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 +102 -0
- package/dist/core/slot/company-metadata.js +536 -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 -25
- package/dist/core/slot/field.js +0 -71
- 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 +17 -0
- package/dist/core/slot/schema-compat.js +103 -0
- package/dist/core/slot/schema.d.ts +353 -76
- package/dist/core/slot/schema.js +163 -10
- package/dist/core/slot/types.d.ts +30 -103
- package/dist/core/slot/value.d.ts +28 -3
- package/dist/core/slot/value.js +93 -57
- package/dist/dev-overlay/protocol.d.ts +74 -8
- package/dist/fn-context/build-context.d.ts +3 -1
- package/dist/fn-context/build-context.js +26 -10
- package/dist/fn-context/fake/empty-api.js +9 -28
- package/dist/fn-context/fake/index.d.ts +13 -4
- package/dist/fn-context/fake/index.js +29 -2
- package/dist/fn-context/fn-params.d.ts +3 -0
- package/dist/fn-context/fn-params.js +10 -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 +20 -9
- package/dist/fn-context/rpc/case-orchestrator-tools.d.ts +32 -0
- package/dist/fn-context/rpc/case-orchestrator-tools.js +12 -0
- package/dist/fn-context/rpc/errors.d.ts +13 -0
- package/dist/fn-context/rpc/errors.js +22 -0
- package/dist/fn-context/rpc/index.d.ts +4 -2
- package/dist/fn-context/rpc/index.js +4 -2
- package/dist/fn-context/rpc/integration/index.d.ts +15 -5
- 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/log.d.ts +11 -1
- package/dist/fn-context/rpc/platform.d.ts +13 -3
- package/dist/fn-context/rpc/platform.js +1 -1
- package/dist/fn-context/rpc/records.d.ts +85 -0
- package/dist/fn-context/rpc/records.js +24 -0
- package/dist/fn-context/rpc/trpc-client.d.ts +44 -2
- package/dist/fn-context/rpc/trpc-client.js +222 -12
- package/dist/fn-context/rpc/trpc-init.d.ts +21 -3
- package/dist/fn-context/rpc/trpc-init.js +10 -4
- package/dist/fn-context/rpc/trpc-router-factory.d.ts +111 -7
- 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 +52 -5
- package/dist/index.d.ts +36 -46
- package/dist/index.js +30 -33
- package/dist/logger.d.ts +11 -0
- package/dist/logger.js +49 -0
- package/dist/platform.d.ts +75 -0
- package/dist/platform.js +57 -0
- package/dist/testing-compile-child.d.ts +2 -0
- package/dist/testing-compile-child.js +20 -0
- package/dist/testing.d.ts +368 -0
- package/dist/testing.js +439 -0
- package/dist/voice-service/actions.d.ts +50 -6
- package/dist/voice-service/index.d.ts +2 -0
- package/dist/voice-service/index.js +2 -0
- package/dist/voice-service/protocol.d.ts +604 -0
- package/dist/voice-service/protocol.js +67 -0
- package/dist/voice-service/types.d.ts +55 -8
- package/dist/worker.d.ts +11 -0
- package/dist/worker.js +11 -0
- package/package.json +38 -15
- 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/functions/func-ref.d.ts +0 -16
- package/dist/core/functions/func-ref.js +0 -20
- 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,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A page's `db` over rows a test gives (the runtime's page unit tests, `testing.ts`): the same reads the
|
|
3
|
+
* platform's reader (services `slot/page/records.ts`) answers from the database, answered from memory.
|
|
4
|
+
* What a where, orderBy, paging, include and select mean, and which reads are refused, is the shared
|
|
5
|
+
* `records-query.ts`; only where the candidate rows come from differs (every row of the table the test
|
|
6
|
+
* gave, in the order it gave them). Two things it does not do: limits on bytes and rows read (a test's
|
|
7
|
+
* data is small), and putting a phone operand in stored form (write test data and where values in
|
|
8
|
+
* E.164, `+14155550123`).
|
|
9
|
+
*/
|
|
10
|
+
import { parentOf, SESSION_TABLE } from '../slot/company-metadata.js';
|
|
11
|
+
import { getCompiledTable, getTableField, relationsOf } from '../slot/compile-schema.js';
|
|
12
|
+
import { emptyFieldValue, makeFieldValue } from '../slot/field.js';
|
|
13
|
+
import { slotFieldNames } from '../slot/row.js';
|
|
14
|
+
import { afterOf, checkListSizes, findAnchors, isPlainObject, limitExceeded, matches, optionalCount, orderKeys, orderRows, pageRows, RECORDS_LIMITS, RecordsQueryError, refuseSearch, relatedKey, relationFilters, RELATION_INCLUDE_KEYS, selectFields, toSlot, unstartableWhy, wayBackFilters, } from './records-query.js';
|
|
15
|
+
const SLOT_STATES = ['filled', 'empty', 'asked_unknown', 'declined'];
|
|
16
|
+
/** A field given as `{ $slot: { value, state, reason } }`, or null for a plain value. */
|
|
17
|
+
function slotGiven(value) {
|
|
18
|
+
if (!isPlainObject(value) || !('$slot' in value))
|
|
19
|
+
return null;
|
|
20
|
+
const slot = value.$slot;
|
|
21
|
+
if (!isPlainObject(slot) || !SLOT_STATES.includes(slot.state)) {
|
|
22
|
+
throw new Error(`renderPage: a $slot is { value, state, reason } with state one of ${SLOT_STATES.join(', ')}`);
|
|
23
|
+
}
|
|
24
|
+
return slot;
|
|
25
|
+
}
|
|
26
|
+
export function createMemoryReader(input) {
|
|
27
|
+
const { schema, rows: given, now } = input;
|
|
28
|
+
const read = new Map();
|
|
29
|
+
const truth = new Map();
|
|
30
|
+
const parents = new Map();
|
|
31
|
+
const calls = [];
|
|
32
|
+
const tableOf = (name) => {
|
|
33
|
+
if (name === SESSION_TABLE)
|
|
34
|
+
throw new RecordsQueryError(`records.${name}: the session is ctx.session, not a table`);
|
|
35
|
+
const table = getCompiledTable(schema, name);
|
|
36
|
+
if (!table)
|
|
37
|
+
throw new RecordsQueryError(`records.${name}: no such table in this sequence`);
|
|
38
|
+
return table;
|
|
39
|
+
};
|
|
40
|
+
// The test's data is checked whole, up front: a mistake in a row the page never reads still shows.
|
|
41
|
+
for (const [name, rows] of Object.entries(given)) {
|
|
42
|
+
const table = getCompiledTable(schema, name);
|
|
43
|
+
if (!table)
|
|
44
|
+
throw new Error(`renderPage: data.${name}: no such table in the company schema`);
|
|
45
|
+
for (const row of rows) {
|
|
46
|
+
if (typeof row.id !== 'string' || !row.id)
|
|
47
|
+
throw new Error(`renderPage: data.${name}: every row needs a string id`);
|
|
48
|
+
for (const key of Object.keys(row)) {
|
|
49
|
+
if (key !== 'id' && key !== 'createdAt' && key !== 'updatedAt' && !getTableField(table, key)) {
|
|
50
|
+
throw new Error(`renderPage: data.${name} row ${row.id}: ${key} is not a field of ${name}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const relationOf = (tableName, name) => relationsOf(schema, tableName).find((r) => r.name === name);
|
|
56
|
+
/** A given row as `db` hands it out: each schema field a slot, filled when the test gave a value. */
|
|
57
|
+
const toRowOut = (tableName, data) => {
|
|
58
|
+
const table = tableOf(tableName);
|
|
59
|
+
const id = String(data.id);
|
|
60
|
+
const out = { id };
|
|
61
|
+
for (const key of ['createdAt', 'updatedAt'])
|
|
62
|
+
if (typeof data[key] === 'string')
|
|
63
|
+
out[key] = data[key];
|
|
64
|
+
for (const field of slotFieldNames(table.fields)) {
|
|
65
|
+
const value = data[field];
|
|
66
|
+
const slot = slotGiven(value);
|
|
67
|
+
const fv = slot
|
|
68
|
+
? makeFieldValue({ value: (slot.value ?? null), state: slot.state, reason: slot.reason ?? null, updatedAt: now })
|
|
69
|
+
: value === undefined || value === null
|
|
70
|
+
? emptyFieldValue(now)
|
|
71
|
+
: makeFieldValue({ value: value, state: 'filled', updatedAt: now });
|
|
72
|
+
out[field] = toSlot(tableName, id, field, fv);
|
|
73
|
+
}
|
|
74
|
+
if (!read.has(id))
|
|
75
|
+
read.set(id, { table: tableName });
|
|
76
|
+
truth.set(id, out);
|
|
77
|
+
return out;
|
|
78
|
+
};
|
|
79
|
+
const allRows = (tableName) => (given[tableName] ?? []).map((data) => toRowOut(tableName, data));
|
|
80
|
+
const noteParents = (tableName, table, field, ids) => {
|
|
81
|
+
if (parentOf(table)?.field !== field)
|
|
82
|
+
return;
|
|
83
|
+
const set = parents.get(tableName) ?? new Set();
|
|
84
|
+
for (const id of ids)
|
|
85
|
+
set.add(id);
|
|
86
|
+
parents.set(tableName, set);
|
|
87
|
+
};
|
|
88
|
+
/** The rows of a way back for each parent, in the order the test gave them. */
|
|
89
|
+
const childrenOf = (relation, parentIds) => {
|
|
90
|
+
const children = allRows(relation.table);
|
|
91
|
+
const out = new Map();
|
|
92
|
+
for (const id of parentIds)
|
|
93
|
+
out.set(id, children.filter((child) => child[relation.field].value === id));
|
|
94
|
+
return out;
|
|
95
|
+
};
|
|
96
|
+
const filterRows = (tableName, rows, where) => {
|
|
97
|
+
const table = tableOf(tableName);
|
|
98
|
+
const relations = relationFilters(table, where);
|
|
99
|
+
const ways = wayBackFilters(tableName, where, relationOf);
|
|
100
|
+
const nested = (target, sub) => sub.some((w) => isPlainObject(w) && (relationFilters(tableOf(target), w).length > 0 || wayBackFilters(target, w, relationOf).length > 0));
|
|
101
|
+
for (const { field, target, sub } of relations) {
|
|
102
|
+
if (nested(target, sub)) {
|
|
103
|
+
throw new RecordsQueryError(`records.${tableName}: where.${field}.is filters one level only; read the related rows first, then filter by their ids (in)`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
for (const { relation, sub } of ways) {
|
|
107
|
+
if (nested(relation.table, sub)) {
|
|
108
|
+
throw new RecordsQueryError(`records.${tableName}: where.${relation.name} filters one level only; read those rows first, then filter by their ids (in)`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const targets = new Map();
|
|
112
|
+
for (const { target } of relations)
|
|
113
|
+
for (const row of allRows(target))
|
|
114
|
+
targets.set(row.id, row);
|
|
115
|
+
const related = new Map();
|
|
116
|
+
for (const { relation } of ways) {
|
|
117
|
+
const key = relatedKey(tableName, relation.name);
|
|
118
|
+
if (!related.has(key))
|
|
119
|
+
related.set(key, childrenOf(relation, rows.map((row) => row.id)));
|
|
120
|
+
}
|
|
121
|
+
const context = { schema, targets, related, relationOf };
|
|
122
|
+
return rows.filter((row) => matches(context, tableName, row, where));
|
|
123
|
+
};
|
|
124
|
+
const includeRelation = (tableName, rows, relation, spec, depth) => {
|
|
125
|
+
const options = isPlainObject(spec) ? spec : {};
|
|
126
|
+
for (const key of Object.keys(options)) {
|
|
127
|
+
if (!RELATION_INCLUDE_KEYS.has(key)) {
|
|
128
|
+
throw new RecordsQueryError(`records.${tableName}: include.${relation.name}.${key} is not supported (where, orderBy, skip, take, include)`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (options.where !== undefined)
|
|
132
|
+
refuseSearch(relation.table, options.where, `include.${relation.name}.where`);
|
|
133
|
+
const children = childrenOf(relation, rows.map((row) => row.id));
|
|
134
|
+
const all = [...children.values()].flat();
|
|
135
|
+
const kept = options.where === undefined ? null : new Set(filterRows(relation.table, all, options.where).map((row) => row.id));
|
|
136
|
+
const childTable = tableOf(relation.table);
|
|
137
|
+
const skip = optionalCount(options.skip, `include.${relation.name}.skip`) ?? 0;
|
|
138
|
+
const take = optionalCount(options.take, `include.${relation.name}.take`);
|
|
139
|
+
const shown = [];
|
|
140
|
+
for (const row of rows) {
|
|
141
|
+
let list = (children.get(row.id) ?? []).filter((child) => !kept || kept.has(child.id));
|
|
142
|
+
list = orderRows(orderKeys(childTable, options.orderBy), list).slice(skip, take === undefined ? undefined : skip + take);
|
|
143
|
+
row[relation.name] = list;
|
|
144
|
+
shown.push(...list);
|
|
145
|
+
}
|
|
146
|
+
include(relation.table, shown, options.include, depth + 1);
|
|
147
|
+
};
|
|
148
|
+
const include = (tableName, rows, spec, depth) => {
|
|
149
|
+
if (spec === undefined || spec === null || spec === false)
|
|
150
|
+
return;
|
|
151
|
+
if (!isPlainObject(spec))
|
|
152
|
+
throw new RecordsQueryError(`records.${tableName}: include must be an object`);
|
|
153
|
+
if (depth > RECORDS_LIMITS.includeDepth)
|
|
154
|
+
throw limitExceeded(`records.${tableName}: include goes deeper than ${RECORDS_LIMITS.includeDepth} levels`);
|
|
155
|
+
const table = tableOf(tableName);
|
|
156
|
+
for (const [field, nested] of Object.entries(spec)) {
|
|
157
|
+
if (nested === false || nested === undefined)
|
|
158
|
+
continue;
|
|
159
|
+
const ref = getTableField(table, field)?.ref;
|
|
160
|
+
if (!ref) {
|
|
161
|
+
const relation = relationOf(tableName, field);
|
|
162
|
+
if (relation) {
|
|
163
|
+
includeRelation(tableName, rows, relation, nested, depth);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
throw new RecordsQueryError(`records.${tableName}: include.${field} is not a lookup nor a way back (relatedName) of ${tableName}`);
|
|
167
|
+
}
|
|
168
|
+
if (ref.kind !== 'lookup')
|
|
169
|
+
throw new RecordsQueryError(`records.${tableName}: include.${field} is the parent; read the parent by id`);
|
|
170
|
+
tableOf(ref.table);
|
|
171
|
+
const byId = new Map(allRows(ref.table).map((row) => [row.id, row]));
|
|
172
|
+
const nestedInclude = isPlainObject(nested) ? nested.include : undefined;
|
|
173
|
+
include(ref.table, [...byId.values()], nestedInclude, depth + 1);
|
|
174
|
+
for (const row of rows) {
|
|
175
|
+
const slot = row[field];
|
|
176
|
+
slot.row = typeof slot.value === 'string' ? (byId.get(slot.value) ?? null) : null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
/** The where as the platform takes it: an email operand lower-cased, as it is stored. */
|
|
181
|
+
const normalizeWhere = (table, where) => {
|
|
182
|
+
const out = {};
|
|
183
|
+
for (const [field, cond] of Object.entries(where)) {
|
|
184
|
+
if (field === 'AND' || field === 'OR' || field === 'NOT') {
|
|
185
|
+
out[field] = Array.isArray(cond) ? cond.map((part) => (isPlainObject(part) ? normalizeWhere(table, part) : part)) : isPlainObject(cond) ? normalizeWhere(table, cond) : cond;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const def = Object.hasOwn(table.fields, field) ? table.fields[field] : undefined;
|
|
189
|
+
out[field] = def?.dataType === 'email' ? lowerEmail(cond) : cond;
|
|
190
|
+
}
|
|
191
|
+
return out;
|
|
192
|
+
};
|
|
193
|
+
const findMany = (tableName, args) => {
|
|
194
|
+
const table = tableOf(tableName);
|
|
195
|
+
const where = normalizeWhere(table, (args.where ?? {}));
|
|
196
|
+
refuseSearch(tableName, where);
|
|
197
|
+
checkListSizes(tableName, where);
|
|
198
|
+
const keys = orderKeys(table, args.orderBy);
|
|
199
|
+
const place = afterOf(tableName, keys, args);
|
|
200
|
+
const anchors = findAnchors(table, where);
|
|
201
|
+
if (anchors.length === 0) {
|
|
202
|
+
const why = unstartableWhy(tableName, table, where);
|
|
203
|
+
if (why)
|
|
204
|
+
throw new RecordsQueryError(why);
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
noteParents(tableName, table, anchors[0].field, anchors[0].ids);
|
|
208
|
+
}
|
|
209
|
+
let rows = filterRows(tableName, allRows(tableName), where);
|
|
210
|
+
rows = orderRows(keys, rows);
|
|
211
|
+
rows = pageRows(keys, rows, place, args);
|
|
212
|
+
include(tableName, rows, args.include, 1);
|
|
213
|
+
return rows.map((row) => selectFields(tableName, table, row, args.select));
|
|
214
|
+
};
|
|
215
|
+
const run = (call) => {
|
|
216
|
+
calls.push(call);
|
|
217
|
+
const args = (call.args ?? {});
|
|
218
|
+
if (!isPlainObject(args))
|
|
219
|
+
throw new RecordsQueryError(`records.${call.table}.${call.method}: arguments must be an object`);
|
|
220
|
+
switch (call.method) {
|
|
221
|
+
case 'findMany':
|
|
222
|
+
return findMany(call.table, args);
|
|
223
|
+
case 'findFirst':
|
|
224
|
+
return findMany(call.table, { ...args, take: 1 })[0] ?? null;
|
|
225
|
+
case 'findUnique': {
|
|
226
|
+
const where = (args.where ?? {});
|
|
227
|
+
const keys = Object.keys(where);
|
|
228
|
+
if (keys.length !== 1 || keys[0] !== 'id' || typeof where.id !== 'string') {
|
|
229
|
+
throw new RecordsQueryError(`records.${call.table}.findUnique: where is { id }`);
|
|
230
|
+
}
|
|
231
|
+
return findMany(call.table, { ...args, take: 1 })[0] ?? null;
|
|
232
|
+
}
|
|
233
|
+
case 'count':
|
|
234
|
+
return findMany(call.table, { where: args.where, take: RECORDS_LIMITS.rowsPerCall }).length;
|
|
235
|
+
default:
|
|
236
|
+
throw new RecordsQueryError(`records.${call.table}: no method ${String(call.method)} (records is read-only)`);
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
return {
|
|
240
|
+
calls,
|
|
241
|
+
async execute(batch) {
|
|
242
|
+
return batch.map((call) => {
|
|
243
|
+
try {
|
|
244
|
+
return { ok: true, value: run(call) };
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
if (error instanceof RecordsQueryError)
|
|
248
|
+
return { ok: false, error: error.message, code: error.code };
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
},
|
|
253
|
+
row(tableName, id) {
|
|
254
|
+
tableOf(tableName);
|
|
255
|
+
const data = (given[tableName] ?? []).find((row) => String(row.id) === id);
|
|
256
|
+
return data ? toRowOut(tableName, data) : null;
|
|
257
|
+
},
|
|
258
|
+
rowInfo(id) {
|
|
259
|
+
const seen = read.get(id);
|
|
260
|
+
const row = truth.get(id);
|
|
261
|
+
if (!seen || !row)
|
|
262
|
+
return undefined;
|
|
263
|
+
const slots = {};
|
|
264
|
+
for (const [field, slot] of Object.entries(row)) {
|
|
265
|
+
if (!slot || typeof slot !== 'object' || Array.isArray(slot))
|
|
266
|
+
continue;
|
|
267
|
+
const { value, state, reason, updatedAt } = slot;
|
|
268
|
+
slots[field] = { value, state, reason, updatedAt };
|
|
269
|
+
}
|
|
270
|
+
return { table: seen.table, slots };
|
|
271
|
+
},
|
|
272
|
+
parentsOf(table) {
|
|
273
|
+
return [...(parents.get(table) ?? [])];
|
|
274
|
+
},
|
|
275
|
+
adoptRow(table, row) {
|
|
276
|
+
read.set(row.id, { table });
|
|
277
|
+
truth.set(row.id, row);
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function lowerEmail(cond) {
|
|
282
|
+
if (typeof cond === 'string')
|
|
283
|
+
return cond.trim().toLowerCase();
|
|
284
|
+
if (!isPlainObject(cond))
|
|
285
|
+
return cond;
|
|
286
|
+
const out = {};
|
|
287
|
+
for (const [op, operand] of Object.entries(cond)) {
|
|
288
|
+
if (op === 'equals' || op === 'startsWith')
|
|
289
|
+
out[op] = typeof operand === 'string' ? operand.trim().toLowerCase() : operand;
|
|
290
|
+
else if (op === 'in' || op === 'notIn')
|
|
291
|
+
out[op] = Array.isArray(operand) ? operand.map((v) => (typeof v === 'string' ? v.trim().toLowerCase() : v)) : operand;
|
|
292
|
+
else if (op === 'not')
|
|
293
|
+
out[op] = lowerEmail(operand);
|
|
294
|
+
else
|
|
295
|
+
out[op] = operand;
|
|
296
|
+
}
|
|
297
|
+
return out;
|
|
298
|
+
}
|
|
299
|
+
//# sourceMappingURL=memory-reader.js.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { SlotState } from './define.js';
|
|
2
|
+
import type { RenderedAction, RenderedCell, RenderedPage } from './render.js';
|
|
3
|
+
import type { RowNames } from './render.js';
|
|
4
|
+
/** `add` / `remove`: options added to or taken from a `multi_select`'s value, the rest kept (27 §3.3). */
|
|
5
|
+
export type SlotOp = {
|
|
6
|
+
slot: string;
|
|
7
|
+
value?: unknown;
|
|
8
|
+
add?: unknown;
|
|
9
|
+
remove?: unknown;
|
|
10
|
+
state?: SlotState;
|
|
11
|
+
reason?: string;
|
|
12
|
+
};
|
|
13
|
+
export type ActionOp = {
|
|
14
|
+
action: string;
|
|
15
|
+
arguments?: Record<string, unknown>;
|
|
16
|
+
};
|
|
17
|
+
export type OpenOp = {
|
|
18
|
+
open: string;
|
|
19
|
+
why?: string;
|
|
20
|
+
};
|
|
21
|
+
export type PageOp = SlotOp | ActionOp | OpenOp;
|
|
22
|
+
export type PageOpsAllowed = {
|
|
23
|
+
/** Slot ids the AI may change (the page's writable cells). */
|
|
24
|
+
slots: readonly string[];
|
|
25
|
+
/** Action ids the AI may press. */
|
|
26
|
+
actions: readonly string[];
|
|
27
|
+
/** Block paths the AI may open; empty: no `open`. */
|
|
28
|
+
open: readonly string[];
|
|
29
|
+
};
|
|
30
|
+
export type ResolvedSlotOp = {
|
|
31
|
+
kind: 'slot';
|
|
32
|
+
cell: RenderedCell;
|
|
33
|
+
value: unknown;
|
|
34
|
+
/** `empty`: cleared (the customer took it back). */
|
|
35
|
+
state: SlotState;
|
|
36
|
+
reason: string | null;
|
|
37
|
+
};
|
|
38
|
+
/** `args`: what the AI filled, rows as ids; the page's prefilled arguments are added when it runs. */
|
|
39
|
+
export type ResolvedActionOp = {
|
|
40
|
+
kind: 'action';
|
|
41
|
+
action: RenderedAction;
|
|
42
|
+
args: Record<string, unknown>;
|
|
43
|
+
};
|
|
44
|
+
export type ResolvedOpenOp = {
|
|
45
|
+
kind: 'open';
|
|
46
|
+
path: string;
|
|
47
|
+
why: string;
|
|
48
|
+
};
|
|
49
|
+
export type ResolvedOp = ResolvedSlotOp | ResolvedActionOp | ResolvedOpenOp;
|
|
50
|
+
export type DroppedOp = {
|
|
51
|
+
op: unknown;
|
|
52
|
+
why: string;
|
|
53
|
+
};
|
|
54
|
+
export declare function resolvePageOps(page: RenderedPage, names: RowNames, answer: unknown, allowed: PageOpsAllowed): {
|
|
55
|
+
ops: ResolvedOp[];
|
|
56
|
+
dropped: DroppedOp[];
|
|
57
|
+
};
|
|
58
|
+
//# sourceMappingURL=ops.d.ts.map
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the AI may do to a page (15 §10.6.2, 17b §4): change a slot (a value, `asked_unknown` /
|
|
3
|
+
* `declined`, or `empty` to clear it; a `multi_select` also by adding or removing options, 27 §3.3) and
|
|
4
|
+
* press an action. The check that turns its operations into ones
|
|
5
|
+
* the platform can run — every operation resolved against the page the AI saw, invalid ones dropped one
|
|
6
|
+
* by one with the reason.
|
|
7
|
+
*/
|
|
8
|
+
import { contactScheduleValueSchema } from '../slot/schema.js';
|
|
9
|
+
import { TIME_VALUE_PATTERN } from '../slot/value.js';
|
|
10
|
+
export function resolvePageOps(page, names, answer, allowed) {
|
|
11
|
+
const ops = [];
|
|
12
|
+
const dropped = [];
|
|
13
|
+
const list = Array.isArray(answer)
|
|
14
|
+
? answer
|
|
15
|
+
: answer && typeof answer === 'object' && Array.isArray(answer.ops)
|
|
16
|
+
? answer.ops
|
|
17
|
+
: null;
|
|
18
|
+
if (!list)
|
|
19
|
+
return { ops, dropped: answer == null ? [] : [{ op: answer, why: 'not { ops: [...] }' }] };
|
|
20
|
+
const slots = new Set(allowed.slots);
|
|
21
|
+
const actions = new Set(allowed.actions);
|
|
22
|
+
const opens = new Set(allowed.open);
|
|
23
|
+
for (const op of list) {
|
|
24
|
+
if (!op || typeof op !== 'object') {
|
|
25
|
+
dropped.push({ op, why: 'not an object' });
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const o = op;
|
|
29
|
+
try {
|
|
30
|
+
if (typeof o.slot === 'string') {
|
|
31
|
+
if (!slots.has(o.slot))
|
|
32
|
+
throw new OpError(`slot ${o.slot} is not editable here`);
|
|
33
|
+
ops.push(resolveSlot(page, names, page.cells[o.slot], o));
|
|
34
|
+
}
|
|
35
|
+
else if (typeof o.action === 'string') {
|
|
36
|
+
if (!actions.has(o.action))
|
|
37
|
+
throw new OpError(`action ${o.action} is not on the page here`);
|
|
38
|
+
const action = page.actions[o.action];
|
|
39
|
+
ops.push({ kind: 'action', action, args: resolveArguments(names, action, o.arguments) });
|
|
40
|
+
}
|
|
41
|
+
else if (typeof o.open === 'string') {
|
|
42
|
+
if (!opens.has(o.open))
|
|
43
|
+
throw new OpError(`cannot open ${o.open}`);
|
|
44
|
+
ops.push({ kind: 'open', path: o.open, why: typeof o.why === 'string' ? o.why : '' });
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
throw new OpError('neither slot, action nor open');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (!(error instanceof OpError))
|
|
52
|
+
throw error;
|
|
53
|
+
dropped.push({ op, why: error.message });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { ops, dropped };
|
|
57
|
+
}
|
|
58
|
+
class OpError extends Error {
|
|
59
|
+
}
|
|
60
|
+
const TIME_RE = new RegExp(TIME_VALUE_PATTERN);
|
|
61
|
+
const STATES = new Set(['filled', 'asked_unknown', 'declined', 'empty']);
|
|
62
|
+
function resolveSlot(page, names, cell, o) {
|
|
63
|
+
const state = (o.state ?? 'filled');
|
|
64
|
+
if (!STATES.has(state))
|
|
65
|
+
throw new OpError(`${cell.slotId}: state ${state} is not filled / asked_unknown / declined / empty`);
|
|
66
|
+
const reason = typeof o.reason === 'string' && o.reason ? o.reason : null;
|
|
67
|
+
const edits = ['add', 'remove'].filter((key) => key in o);
|
|
68
|
+
if (edits.length && cell.type !== 'multi_select')
|
|
69
|
+
throw new OpError(`${cell.slotId}: ${edits.join(' / ')} is for a multi_select field; write value`);
|
|
70
|
+
if (state !== 'filled') {
|
|
71
|
+
if (edits.length)
|
|
72
|
+
throw new OpError(`${cell.slotId}: ${edits.join(' / ')} goes with state filled`);
|
|
73
|
+
return { kind: 'slot', cell, value: null, state: state, reason };
|
|
74
|
+
}
|
|
75
|
+
if (cell.type === 'multi_select')
|
|
76
|
+
return { kind: 'slot', cell, value: multiSelectValue(cell, o, edits), state: 'filled', reason };
|
|
77
|
+
if (!('value' in o) || o.value === null || o.value === undefined || o.value === '') {
|
|
78
|
+
throw new OpError(`${cell.slotId}: filled needs a value`);
|
|
79
|
+
}
|
|
80
|
+
let value = o.value;
|
|
81
|
+
if (cell.options) {
|
|
82
|
+
// A lookup: the AI names one of the rows on the page.
|
|
83
|
+
if (typeof value !== 'string' || !cell.options.includes(value)) {
|
|
84
|
+
throw new OpError(`${cell.slotId}: ${JSON.stringify(value)} is not one of ${cell.options.join(', ') || '(no rows on the page)'}`);
|
|
85
|
+
}
|
|
86
|
+
const row = names.resolve(value);
|
|
87
|
+
if (!row)
|
|
88
|
+
throw new OpError(`${cell.slotId}: ${value} is not a row on the page`);
|
|
89
|
+
value = row.id;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
value = coerce(cell.slotId, cell.type, cell.choices, value);
|
|
93
|
+
}
|
|
94
|
+
return { kind: 'slot', cell, value, state: 'filled', reason };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* A `multi_select`'s new value (27 §3.3): `value` replaces the list, `add` / `remove` change the one
|
|
98
|
+
* the AI saw on the page and keep the rest. In the options' order, each once.
|
|
99
|
+
*/
|
|
100
|
+
function multiSelectValue(cell, o, edits) {
|
|
101
|
+
const choices = cell.choices ?? [];
|
|
102
|
+
const given = edits.length ? edits : ['value'];
|
|
103
|
+
if (given.length + (edits.length && 'value' in o ? 1 : 0) > 1)
|
|
104
|
+
throw new OpError(`${cell.slotId}: one of value, add or remove at a time`);
|
|
105
|
+
const key = given[0];
|
|
106
|
+
const raw = o[key];
|
|
107
|
+
if (raw === null || raw === undefined)
|
|
108
|
+
throw new OpError(`${cell.slotId}: filled needs ${key}`);
|
|
109
|
+
// One option alone is taken as a list of one.
|
|
110
|
+
const list = Array.isArray(raw) ? raw : [raw];
|
|
111
|
+
const outside = list.filter((item) => typeof item !== 'string' || !choices.includes(item));
|
|
112
|
+
if (outside.length) {
|
|
113
|
+
throw new OpError(`${cell.slotId}: ${outside.map((v) => JSON.stringify(v)).join(', ')} not one of ${choices.map((v) => JSON.stringify(v)).join(', ')}`);
|
|
114
|
+
}
|
|
115
|
+
const now = cell.state === 'filled' && Array.isArray(cell.value) ? cell.value : [];
|
|
116
|
+
const next = new Set(key === 'value' ? list : now);
|
|
117
|
+
if (key === 'add')
|
|
118
|
+
for (const item of list)
|
|
119
|
+
next.add(item);
|
|
120
|
+
if (key === 'remove')
|
|
121
|
+
for (const item of list)
|
|
122
|
+
next.delete(item);
|
|
123
|
+
return choices.filter((choice) => next.has(choice));
|
|
124
|
+
}
|
|
125
|
+
function resolveArguments(names, action, raw) {
|
|
126
|
+
if (!action.input) {
|
|
127
|
+
if (raw && typeof raw === 'object' && Object.keys(raw).length)
|
|
128
|
+
throw new OpError(`${action.id}: takes no arguments`);
|
|
129
|
+
return {};
|
|
130
|
+
}
|
|
131
|
+
return resolveObject(names, action.id, action.input.properties, action.input.required, raw ?? {});
|
|
132
|
+
}
|
|
133
|
+
function resolveObject(names, where, properties, required, raw) {
|
|
134
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
135
|
+
throw new OpError(`${where}: arguments must be an object`);
|
|
136
|
+
const out = {};
|
|
137
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
138
|
+
const prop = properties[key];
|
|
139
|
+
if (!prop)
|
|
140
|
+
throw new OpError(`${where}: ${key} is not an argument here`);
|
|
141
|
+
if (value === null || value === undefined || value === '')
|
|
142
|
+
continue;
|
|
143
|
+
out[key] = resolveValue(names, `${where}.${key}`, prop, value);
|
|
144
|
+
}
|
|
145
|
+
for (const key of required ?? []) {
|
|
146
|
+
if (!(key in out))
|
|
147
|
+
throw new OpError(`${where}: ${key} is required`);
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
function resolveValue(names, where, prop, value) {
|
|
152
|
+
if (prop.type === 'array') {
|
|
153
|
+
if (!Array.isArray(value))
|
|
154
|
+
throw new OpError(`${where}: must be a list`);
|
|
155
|
+
return value.map((item, i) => resolveValue(names, `${where}[${i}]`, prop.items, item));
|
|
156
|
+
}
|
|
157
|
+
if (prop.type === 'object' && prop['x-type'] === 'contact_schedule') {
|
|
158
|
+
// Checked as the field checks it; `run` gets the schedule as it is.
|
|
159
|
+
const parsed = contactScheduleValueSchema.safeParse(value);
|
|
160
|
+
if (parsed.success)
|
|
161
|
+
return value;
|
|
162
|
+
const issue = parsed.error.issues[0];
|
|
163
|
+
throw new OpError(`${where}: not a contact schedule (${issue.path.join('.') || 'value'}: ${issue.message})`);
|
|
164
|
+
}
|
|
165
|
+
if (prop.type === 'object')
|
|
166
|
+
return resolveObject(names, where, prop.properties, prop.required, value);
|
|
167
|
+
if (prop.type === 'ref') {
|
|
168
|
+
// A row choice: one of the rows on the page, by page name; `run` gets its id.
|
|
169
|
+
const row = typeof value === 'string' && prop.enum.includes(value) ? names.resolve(value) : undefined;
|
|
170
|
+
if (!row)
|
|
171
|
+
throw new OpError(`${where}: must be one of ${prop.enum.join(', ') || '(no rows on the page)'}`);
|
|
172
|
+
return row.id;
|
|
173
|
+
}
|
|
174
|
+
if (prop.type === 'multi_select') {
|
|
175
|
+
// As a fill writes it: one option alone is a list of one; each once, in the options' order.
|
|
176
|
+
const list = Array.isArray(value) ? value : [value];
|
|
177
|
+
const outside = list.filter((item) => !prop.enum.includes(item));
|
|
178
|
+
if (outside.length)
|
|
179
|
+
throw new OpError(`${where}: ${outside.map((v) => JSON.stringify(v)).join(', ')} not one of ${prop.enum.map((v) => JSON.stringify(v)).join(', ')}`);
|
|
180
|
+
return prop.enum.filter((option) => list.includes(option));
|
|
181
|
+
}
|
|
182
|
+
// A schema field's argument is checked as a fill of the field is.
|
|
183
|
+
return coerce(where, prop.type === 'integer' ? 'number' : prop.type, prop.enum ?? null, value);
|
|
184
|
+
}
|
|
185
|
+
function coerce(where, type, enumValues, value) {
|
|
186
|
+
let out = value;
|
|
187
|
+
if (type === 'number') {
|
|
188
|
+
out = typeof value === 'string' && value.trim() !== '' && Number.isFinite(Number(value)) ? Number(value) : value;
|
|
189
|
+
if (typeof out !== 'number' || !Number.isFinite(out))
|
|
190
|
+
throw new OpError(`${where}: ${JSON.stringify(value)} is not a number`);
|
|
191
|
+
}
|
|
192
|
+
else if (type === 'boolean') {
|
|
193
|
+
out = value === 'true' ? true : value === 'false' ? false : value;
|
|
194
|
+
if (typeof out !== 'boolean')
|
|
195
|
+
throw new OpError(`${where}: ${JSON.stringify(value)} is not true / false`);
|
|
196
|
+
}
|
|
197
|
+
else if (type === 'date') {
|
|
198
|
+
if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value))
|
|
199
|
+
throw new OpError(`${where}: ${JSON.stringify(value)} is not YYYY-MM-DD`);
|
|
200
|
+
}
|
|
201
|
+
else if (type === 'datetime') {
|
|
202
|
+
if (typeof value !== 'string' || Number.isNaN(Date.parse(value)))
|
|
203
|
+
throw new OpError(`${where}: ${JSON.stringify(value)} is not a date-time`);
|
|
204
|
+
}
|
|
205
|
+
else if (type === 'time') {
|
|
206
|
+
if (typeof value !== 'string' || !TIME_RE.test(value))
|
|
207
|
+
throw new OpError(`${where}: ${JSON.stringify(value)} is not a time of day HH:mm, 24-hour (15:30)`);
|
|
208
|
+
}
|
|
209
|
+
else if (type === 'string' || type === 'select' || type === 'text' || type === 'phone' || type === 'email') {
|
|
210
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
211
|
+
out = String(value);
|
|
212
|
+
if (typeof out !== 'string')
|
|
213
|
+
throw new OpError(`${where}: ${JSON.stringify(value)} is not text`);
|
|
214
|
+
}
|
|
215
|
+
if (enumValues && !enumValues.includes(out)) {
|
|
216
|
+
throw new OpError(`${where}: ${JSON.stringify(value)} is not one of ${enumValues.map((v) => JSON.stringify(v)).join(', ')}`);
|
|
217
|
+
}
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
//# sourceMappingURL=ops.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { HostResult } from './sandbox.js';
|
|
2
|
+
/** The zone a page reads time in when the contact gives none. */
|
|
3
|
+
export declare const DEFAULT_PAGE_ZONE = "America/New_York";
|
|
4
|
+
/** A row as `db` hands it out, as far as its time zone goes: each field a slot with a `value`. */
|
|
5
|
+
type CallerRow = Record<string, unknown> | null;
|
|
6
|
+
/** The contact's zone (their `timezone`, else from postal code, state or phone) and today there (25 §11). */
|
|
7
|
+
export declare function timeOfCaller(caller: CallerRow, now: Date): {
|
|
8
|
+
zone: string;
|
|
9
|
+
today: string;
|
|
10
|
+
};
|
|
11
|
+
/** `ctx.time.format` (25 §11). */
|
|
12
|
+
export declare function timeCall(args: unknown[]): HostResult;
|
|
13
|
+
/** `ctx.time.day` (25 §11). */
|
|
14
|
+
export declare function dayCall(args: unknown[]): HostResult;
|
|
15
|
+
/** `ctx.time.at` (74 §4.7): a day and a wall time in a zone, as an instant. */
|
|
16
|
+
export declare function atCall(args: unknown[]): HostResult;
|
|
17
|
+
/** `ctx.time.parts` (74 §4.7): an instant's wall-clock parts in a zone. */
|
|
18
|
+
export declare function partsCall(args: unknown[]): HostResult;
|
|
19
|
+
export {};
|
|
20
|
+
//# sourceMappingURL=page-host.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The page script's host calls that read no data (25 §11, 74 §4.7): `ctx.time.format`, `.day`, `.at` and `.parts`, done
|
|
3
|
+
* outside the sandbox where `Intl` is, and the caller's zone and today. The platform's render and the
|
|
4
|
+
* runtime's page unit tests answer them with these.
|
|
5
|
+
*/
|
|
6
|
+
import { contactTimezone } from '../slot/contact-timezone.js';
|
|
7
|
+
import { dayIn, formatTime, timeAt, timeParts } from './time-format.js';
|
|
8
|
+
/** The zone a page reads time in when the contact gives none. */
|
|
9
|
+
export const DEFAULT_PAGE_ZONE = 'America/New_York';
|
|
10
|
+
/** The contact's zone (their `timezone`, else from postal code, state or phone) and today there (25 §11). */
|
|
11
|
+
export function timeOfCaller(caller, now) {
|
|
12
|
+
const value = (name) => caller?.[name]?.value;
|
|
13
|
+
const zone = contactTimezone({ timezone: value('timezone'), postal_code: value('postal_code'), state: value('state'), country: value('country'), phone: value('phone') }, DEFAULT_PAGE_ZONE).timezone;
|
|
14
|
+
return { zone, today: dayIn(now, zone) };
|
|
15
|
+
}
|
|
16
|
+
/** `ctx.time.format` (25 §11). */
|
|
17
|
+
export function timeCall(args) {
|
|
18
|
+
const [at, options] = args;
|
|
19
|
+
try {
|
|
20
|
+
if (typeof at !== 'string' && typeof at !== 'number')
|
|
21
|
+
throw new Error('time.format: give an ISO time');
|
|
22
|
+
return { ok: true, value: formatTime(at, { timeZone: String(options?.timeZone ?? DEFAULT_PAGE_ZONE), ...(options?.style ? { style: options.style } : {}) }) };
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** `ctx.time.day` (25 §11). */
|
|
29
|
+
export function dayCall(args) {
|
|
30
|
+
const [at, timeZone] = args;
|
|
31
|
+
try {
|
|
32
|
+
if (typeof at !== 'string' && typeof at !== 'number')
|
|
33
|
+
throw new Error('time.day: give an ISO time');
|
|
34
|
+
return { ok: true, value: dayIn(at, String(timeZone ?? DEFAULT_PAGE_ZONE)) };
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** `ctx.time.at` / `ctx.time.parts` (74 §4.7). */
|
|
41
|
+
function hostTime(work) {
|
|
42
|
+
try {
|
|
43
|
+
return { ok: true, value: work() };
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** `ctx.time.at` (74 §4.7): a day and a wall time in a zone, as an instant. */
|
|
50
|
+
export function atCall(args) {
|
|
51
|
+
return hostTime(() => timeAt(String(args[0]), String(args[1]), String(args[2] ?? DEFAULT_PAGE_ZONE)));
|
|
52
|
+
}
|
|
53
|
+
/** `ctx.time.parts` (74 §4.7): an instant's wall-clock parts in a zone. */
|
|
54
|
+
export function partsCall(args) {
|
|
55
|
+
return hostTime(() => timeParts(args[0], String(args[1] ?? DEFAULT_PAGE_ZONE)));
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=page-host.js.map
|