@ekairos/domain 1.22.35-beta.development.0 → 1.22.35
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 +177 -18
- package/SKILL.md +56 -0
- package/dist/cli/bin.d.ts +1 -1
- package/dist/cli/bin.d.ts.map +1 -1
- package/dist/cli/bin.js +141 -20
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/create-app.d.ts +34 -1
- package/dist/cli/create-app.d.ts.map +1 -1
- package/dist/cli/create-app.js +2138 -507
- package/dist/cli/create-app.js.map +1 -1
- package/dist/cli/http.d.ts.map +1 -1
- package/dist/cli/http.js +1 -5
- package/dist/cli/http.js.map +1 -1
- package/dist/cli/server.d.ts.map +1 -1
- package/dist/cli/server.js +5 -2
- package/dist/cli/server.js.map +1 -1
- package/dist/cli/types.d.ts +1 -0
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/cli/ui.d.ts.map +1 -1
- package/dist/cli/ui.js +2 -0
- package/dist/cli/ui.js.map +1 -1
- package/dist/context.test-runner.js +3 -1
- package/dist/context.test-runner.js.map +1 -1
- package/dist/domain-doc.d.ts +2 -0
- package/dist/domain-doc.d.ts.map +1 -1
- package/dist/domain-doc.js +14 -0
- package/dist/domain-doc.js.map +1 -1
- package/dist/index.d.ts +194 -53
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +313 -137
- package/dist/index.js.map +1 -1
- package/dist/next.d.ts.map +1 -1
- package/dist/next.js +3 -2
- package/dist/next.js.map +1 -1
- package/dist/runtime-handle.d.ts +14 -3
- package/dist/runtime-handle.d.ts.map +1 -1
- package/dist/runtime-handle.js +2 -0
- package/dist/runtime-handle.js.map +1 -1
- package/dist/runtime-step.d.ts.map +1 -1
- package/dist/runtime-step.js +2 -0
- package/dist/runtime-step.js.map +1 -1
- package/dist/runtime.d.ts +7 -7
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +11 -8
- package/dist/runtime.js.map +1 -1
- package/package.json +15 -11
package/dist/index.js
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
import { i } from "@instantdb/core";
|
|
2
|
-
import {
|
|
3
|
-
export { parseDomainDoc, renderDomainDoc, filterDomainDoc, } from "./domain-doc.js";
|
|
2
|
+
import { z } from "zod";
|
|
4
3
|
export { EkairosRuntime, } from "./runtime-handle.js";
|
|
5
4
|
let domainDocLoader = null;
|
|
5
|
+
let domainDocNormalizer = null;
|
|
6
6
|
export function configureDomainDocLoader(loader) {
|
|
7
7
|
domainDocLoader = loader ?? null;
|
|
8
8
|
}
|
|
9
|
+
export function configureDomainDocNormalizer(normalizer) {
|
|
10
|
+
domainDocNormalizer = normalizer ?? null;
|
|
11
|
+
}
|
|
9
12
|
const EKAIROS_META = Symbol.for("@ekairos/domain/meta");
|
|
10
13
|
const EKAIROS_ACTIONS = Symbol.for("@ekairos/domain/actions");
|
|
11
14
|
const EKAIROS_ACTION_MAP = Symbol.for("@ekairos/domain/action-map");
|
|
12
15
|
const EKAIROS_ACTION_BINDING = Symbol.for("@ekairos/domain/action-binding");
|
|
13
|
-
const EKAIROS_ACTION_STACK = Symbol.for("@ekairos/domain/action-stack");
|
|
14
16
|
function getMeta(source) {
|
|
15
|
-
if (!source
|
|
17
|
+
if (!isObjectLike(source))
|
|
16
18
|
return null;
|
|
17
19
|
return source[EKAIROS_META] ?? null;
|
|
18
20
|
}
|
|
19
21
|
function getActionBinding(source) {
|
|
20
|
-
if (!source
|
|
22
|
+
if (!isObjectLike(source))
|
|
21
23
|
return null;
|
|
22
24
|
const binding = source[EKAIROS_ACTION_BINDING];
|
|
23
25
|
if (!binding || typeof binding !== "object")
|
|
@@ -50,7 +52,7 @@ function bindAction(action, params) {
|
|
|
50
52
|
return registration;
|
|
51
53
|
}
|
|
52
54
|
function getStoredActions(source) {
|
|
53
|
-
if (!source
|
|
55
|
+
if (!isObjectLike(source))
|
|
54
56
|
return [];
|
|
55
57
|
const raw = source[EKAIROS_ACTIONS];
|
|
56
58
|
if (!Array.isArray(raw))
|
|
@@ -61,7 +63,7 @@ function getStoredActions(source) {
|
|
|
61
63
|
typeof entry.execute === "function");
|
|
62
64
|
}
|
|
63
65
|
function getStoredActionMap(source) {
|
|
64
|
-
if (!source
|
|
66
|
+
if (!isObjectLike(source))
|
|
65
67
|
return {};
|
|
66
68
|
const raw = source[EKAIROS_ACTION_MAP];
|
|
67
69
|
if (!raw || typeof raw !== "object")
|
|
@@ -69,7 +71,7 @@ function getStoredActionMap(source) {
|
|
|
69
71
|
return raw;
|
|
70
72
|
}
|
|
71
73
|
function setStoredActions(source, actions) {
|
|
72
|
-
if (!source
|
|
74
|
+
if (!isObjectLike(source))
|
|
73
75
|
return;
|
|
74
76
|
const frozenActions = Object.freeze([...actions]);
|
|
75
77
|
Object.defineProperty(source, EKAIROS_ACTIONS, {
|
|
@@ -80,7 +82,7 @@ function setStoredActions(source, actions) {
|
|
|
80
82
|
});
|
|
81
83
|
}
|
|
82
84
|
function setStoredActionMap(source, actionMap) {
|
|
83
|
-
if (!source
|
|
85
|
+
if (!isObjectLike(source))
|
|
84
86
|
return;
|
|
85
87
|
Object.defineProperty(source, EKAIROS_ACTION_MAP, {
|
|
86
88
|
value: Object.freeze({ ...actionMap }),
|
|
@@ -89,29 +91,13 @@ function setStoredActionMap(source, actionMap) {
|
|
|
89
91
|
writable: true,
|
|
90
92
|
});
|
|
91
93
|
}
|
|
92
|
-
function readRuntimeActionStack(runtime) {
|
|
93
|
-
if (!runtime || typeof runtime !== "object")
|
|
94
|
-
return [];
|
|
95
|
-
const stack = runtime[EKAIROS_ACTION_STACK];
|
|
96
|
-
return Array.isArray(stack) ? [...stack] : [];
|
|
97
|
-
}
|
|
98
|
-
function cloneRuntimeWithActionStack(runtime, stack) {
|
|
99
|
-
if (!runtime || typeof runtime !== "object")
|
|
100
|
-
return runtime;
|
|
101
|
-
const scoped = Object.assign(Object.create(Object.getPrototypeOf(runtime)), runtime);
|
|
102
|
-
Object.defineProperty(scoped, EKAIROS_ACTION_STACK, {
|
|
103
|
-
value: [...stack],
|
|
104
|
-
enumerable: false,
|
|
105
|
-
configurable: true,
|
|
106
|
-
writable: true,
|
|
107
|
-
});
|
|
108
|
-
return scoped;
|
|
109
|
-
}
|
|
110
94
|
function normalizeActionLike(value, params) {
|
|
111
|
-
const action =
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
95
|
+
const action = value;
|
|
96
|
+
if (!action ||
|
|
97
|
+
typeof action !== "object" ||
|
|
98
|
+
typeof action.execute !== "function" ||
|
|
99
|
+
!action.input ||
|
|
100
|
+
!action.output) {
|
|
115
101
|
throw new Error(`Invalid domain action definition: ${params.fallbackName}`);
|
|
116
102
|
}
|
|
117
103
|
const explicitName = typeof action.name === "string" ? action.name.trim() : "";
|
|
@@ -241,9 +227,22 @@ function listKeys(value) {
|
|
|
241
227
|
return [];
|
|
242
228
|
return Object.keys(value).filter((key) => !key.startsWith("$"));
|
|
243
229
|
}
|
|
230
|
+
function isObjectLike(value) {
|
|
231
|
+
return !!value && (typeof value === "object" || typeof value === "function");
|
|
232
|
+
}
|
|
233
|
+
function isMaterializedDomainSource(value) {
|
|
234
|
+
if (!isObjectLike(value))
|
|
235
|
+
return false;
|
|
236
|
+
const source = value;
|
|
237
|
+
return (typeof source.instantSchema === "function" ||
|
|
238
|
+
typeof source.toInstantSchema === "function" ||
|
|
239
|
+
("entities" in source && "links" in source && "rooms" in source));
|
|
240
|
+
}
|
|
244
241
|
function resolveSchema(source) {
|
|
245
242
|
if (!source)
|
|
246
243
|
return null;
|
|
244
|
+
if (typeof source.instantSchema === "function")
|
|
245
|
+
return source.instantSchema();
|
|
247
246
|
if (typeof source.toInstantSchema === "function")
|
|
248
247
|
return source.toInstantSchema();
|
|
249
248
|
if (typeof source.schema === "function")
|
|
@@ -280,6 +279,48 @@ function assertSchemaIncludes(fullSchema, requiredSchema) {
|
|
|
280
279
|
throw new Error(`ConcreteDomain: schema is missing required keys (${parts.join(" | ")})`);
|
|
281
280
|
}
|
|
282
281
|
}
|
|
282
|
+
function collectTransitiveDomainNames(source, seen = new Set()) {
|
|
283
|
+
const names = new Set();
|
|
284
|
+
if (!isObjectLike(source))
|
|
285
|
+
return names;
|
|
286
|
+
if (seen.has(source))
|
|
287
|
+
return names;
|
|
288
|
+
seen.add(source);
|
|
289
|
+
const meta = getMeta(source);
|
|
290
|
+
if (!meta)
|
|
291
|
+
return names;
|
|
292
|
+
if (meta.name)
|
|
293
|
+
names.add(meta.name);
|
|
294
|
+
for (const getter of meta.includes ?? []) {
|
|
295
|
+
if (!getter)
|
|
296
|
+
continue;
|
|
297
|
+
let child = null;
|
|
298
|
+
try {
|
|
299
|
+
child = getter();
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
child = null;
|
|
303
|
+
}
|
|
304
|
+
for (const name of collectTransitiveDomainNames(child, seen)) {
|
|
305
|
+
names.add(name);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return names;
|
|
309
|
+
}
|
|
310
|
+
function assertDomainNamesInclude(rootDomain, requiredDomain) {
|
|
311
|
+
const rootMeta = getMeta(rootDomain);
|
|
312
|
+
const requiredMeta = getMeta(requiredDomain);
|
|
313
|
+
if (!rootMeta || !requiredMeta)
|
|
314
|
+
return;
|
|
315
|
+
const rootNames = collectTransitiveDomainNames(rootDomain);
|
|
316
|
+
const requiredNames = collectTransitiveDomainNames(requiredDomain);
|
|
317
|
+
if (rootNames.size === 0 || requiredNames.size === 0)
|
|
318
|
+
return;
|
|
319
|
+
const missing = Array.from(requiredNames).filter((name) => !rootNames.has(name));
|
|
320
|
+
if (missing.length > 0) {
|
|
321
|
+
throw new Error(`ConcreteDomain: domain is missing required names (${missing.join(", ")})`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
283
324
|
function createConcreteDomain(domainInstance, db, fullSchema, bindings) {
|
|
284
325
|
const baseSchema = fullSchema ?? resolveSchema(domainInstance);
|
|
285
326
|
const actionMap = getStoredActionMap(domainInstance);
|
|
@@ -290,8 +331,16 @@ function createConcreteDomain(domainInstance, db, fullSchema, bindings) {
|
|
|
290
331
|
context: (options) => domainInstance.context(options),
|
|
291
332
|
contextString: (options) => domainInstance.contextString(options),
|
|
292
333
|
};
|
|
293
|
-
if (bindings?.
|
|
294
|
-
const inheritedStack =
|
|
334
|
+
if (bindings?.runtime !== undefined) {
|
|
335
|
+
const inheritedStack = [];
|
|
336
|
+
const createActionRuntime = (stack) => {
|
|
337
|
+
const runtime = {
|
|
338
|
+
...concrete,
|
|
339
|
+
...(bindings.env !== undefined ? { env: bindings.env } : {}),
|
|
340
|
+
};
|
|
341
|
+
runtime.actions = buildActions(stack);
|
|
342
|
+
return runtime;
|
|
343
|
+
};
|
|
295
344
|
const buildActions = (stack) => Object.fromEntries(Object.entries(actionMap).map(([key, action]) => [
|
|
296
345
|
key,
|
|
297
346
|
async (input) => {
|
|
@@ -303,25 +352,51 @@ function createConcreteDomain(domainInstance, db, fullSchema, bindings) {
|
|
|
303
352
|
throw new Error(`domain_action_cycle:${key}`);
|
|
304
353
|
}
|
|
305
354
|
const nextStack = [...stack, key];
|
|
306
|
-
const scopedRuntime =
|
|
355
|
+
const scopedRuntime = createActionRuntime(nextStack);
|
|
356
|
+
const parsedInput = action.input.parse(input);
|
|
307
357
|
const params = {
|
|
308
|
-
|
|
309
|
-
input,
|
|
358
|
+
input: parsedInput,
|
|
310
359
|
runtime: scopedRuntime,
|
|
311
360
|
};
|
|
312
|
-
|
|
361
|
+
const output = await execute(params);
|
|
362
|
+
return action.output.parse(output);
|
|
313
363
|
},
|
|
314
364
|
]));
|
|
315
|
-
|
|
316
|
-
|
|
365
|
+
if (bindings.env !== undefined) {
|
|
366
|
+
;
|
|
367
|
+
concrete.env = bindings.env;
|
|
368
|
+
}
|
|
317
369
|
;
|
|
318
370
|
concrete.actions = buildActions(inheritedStack);
|
|
319
371
|
}
|
|
320
372
|
return concrete;
|
|
321
373
|
}
|
|
374
|
+
function promoteRuntimeDomainScope(scoped) {
|
|
375
|
+
const promoted = { ...scoped };
|
|
376
|
+
const db = scoped?.db;
|
|
377
|
+
if (db && typeof db.query === "function") {
|
|
378
|
+
promoted.query = db.query.bind(db);
|
|
379
|
+
}
|
|
380
|
+
const actions = scoped?.actions;
|
|
381
|
+
if (actions && typeof actions === "object") {
|
|
382
|
+
for (const [key, action] of Object.entries(actions)) {
|
|
383
|
+
if (key in promoted)
|
|
384
|
+
continue;
|
|
385
|
+
promoted[key] = action;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return promoted;
|
|
389
|
+
}
|
|
390
|
+
async function callDomainRuntimeScope(domainInstance, runtime, options) {
|
|
391
|
+
if (!runtime || typeof runtime.use !== "function") {
|
|
392
|
+
throw new Error("domain(runtime) requires an Ekairos runtime with use(domain).");
|
|
393
|
+
}
|
|
394
|
+
return promoteRuntimeDomainScope(await runtime.use(domainInstance, options));
|
|
395
|
+
}
|
|
322
396
|
export function materializeDomain(params) {
|
|
323
397
|
const baseSchema = resolveSchema(params.rootDomain);
|
|
324
398
|
const requiredSchema = resolveSchema(params.subdomain);
|
|
399
|
+
assertDomainNamesInclude(params.rootDomain, params.subdomain);
|
|
325
400
|
assertSchemaIncludes(baseSchema, requiredSchema);
|
|
326
401
|
return createConcreteDomain(params.subdomain, params.db, baseSchema, params.bindings);
|
|
327
402
|
}
|
|
@@ -338,18 +413,18 @@ function loadDomainDoc(scope, meta) {
|
|
|
338
413
|
function normalizeDoc(docInfo, options) {
|
|
339
414
|
if (!docInfo?.doc)
|
|
340
415
|
return { doc: null, docPath: docInfo?.docPath };
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
}
|
|
352
|
-
return { doc:
|
|
416
|
+
if (domainDocNormalizer) {
|
|
417
|
+
try {
|
|
418
|
+
const normalized = domainDocNormalizer({ docInfo, options });
|
|
419
|
+
if (normalized)
|
|
420
|
+
return normalized;
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
// Fall through to raw docs. Domain context must remain usable without the
|
|
424
|
+
// optional markdown/YAML parser in workflow bundles.
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return { doc: docInfo.doc, docPath: docInfo.docPath };
|
|
353
428
|
}
|
|
354
429
|
function buildRegistryEntries(meta, options) {
|
|
355
430
|
if (!meta)
|
|
@@ -368,7 +443,7 @@ function buildRegistryEntries(meta, options) {
|
|
|
368
443
|
catch {
|
|
369
444
|
child = null;
|
|
370
445
|
}
|
|
371
|
-
if (!child
|
|
446
|
+
if (!isObjectLike(child))
|
|
372
447
|
continue;
|
|
373
448
|
if (seen.has(child))
|
|
374
449
|
continue;
|
|
@@ -490,7 +565,7 @@ function resolveIncludeNames(meta) {
|
|
|
490
565
|
catch {
|
|
491
566
|
child = null;
|
|
492
567
|
}
|
|
493
|
-
if (!child
|
|
568
|
+
if (!isObjectLike(child))
|
|
494
569
|
continue;
|
|
495
570
|
const childMeta = getMeta(child);
|
|
496
571
|
if (childMeta?.name)
|
|
@@ -498,6 +573,64 @@ function resolveIncludeNames(meta) {
|
|
|
498
573
|
}
|
|
499
574
|
return Array.from(names);
|
|
500
575
|
}
|
|
576
|
+
function isRuntimeEntityDef(value) {
|
|
577
|
+
return Boolean(value &&
|
|
578
|
+
typeof value === "object" &&
|
|
579
|
+
"attrs" in value &&
|
|
580
|
+
value.attrs &&
|
|
581
|
+
typeof value.attrs === "object");
|
|
582
|
+
}
|
|
583
|
+
function stripRuntimeEntityLinks(entity) {
|
|
584
|
+
if (!isRuntimeEntityDef(entity))
|
|
585
|
+
return entity;
|
|
586
|
+
return i.entity({ ...entity.attrs });
|
|
587
|
+
}
|
|
588
|
+
function normalizeRuntimeAttrDef(value) {
|
|
589
|
+
if (!value || typeof value !== "object") {
|
|
590
|
+
return value;
|
|
591
|
+
}
|
|
592
|
+
const record = value;
|
|
593
|
+
const sorted = {};
|
|
594
|
+
for (const key of Object.keys(record).sort()) {
|
|
595
|
+
sorted[key] = normalizeRuntimeAttrDef(record[key]);
|
|
596
|
+
}
|
|
597
|
+
return sorted;
|
|
598
|
+
}
|
|
599
|
+
function stableRuntimeAttrDef(value) {
|
|
600
|
+
return JSON.stringify(normalizeRuntimeAttrDef(value));
|
|
601
|
+
}
|
|
602
|
+
function areRuntimeAttrDefsEquivalent(baseAttr, nextAttr) {
|
|
603
|
+
if (baseAttr === nextAttr)
|
|
604
|
+
return true;
|
|
605
|
+
return stableRuntimeAttrDef(baseAttr) === stableRuntimeAttrDef(nextAttr);
|
|
606
|
+
}
|
|
607
|
+
function mergeRuntimeEntityDefs(entityName, baseEntity, nextEntity) {
|
|
608
|
+
if (!isRuntimeEntityDef(baseEntity) || !isRuntimeEntityDef(nextEntity)) {
|
|
609
|
+
return stripRuntimeEntityLinks(nextEntity);
|
|
610
|
+
}
|
|
611
|
+
const conflictingAttrs = Object.keys(nextEntity.attrs).filter((attr) => Object.prototype.hasOwnProperty.call(baseEntity.attrs, attr) &&
|
|
612
|
+
!areRuntimeAttrDefsEquivalent(baseEntity.attrs[attr], nextEntity.attrs[attr]));
|
|
613
|
+
if (conflictingAttrs.length > 0) {
|
|
614
|
+
throw new Error(`domain_duplicate_entity_attr:${entityName}.${conflictingAttrs.join(",")}`);
|
|
615
|
+
}
|
|
616
|
+
return i.entity({
|
|
617
|
+
...baseEntity.attrs,
|
|
618
|
+
...nextEntity.attrs,
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
function mergeRuntimeEntities(baseEntities, nextEntities) {
|
|
622
|
+
const merged = {};
|
|
623
|
+
for (const [entityName, entity] of Object.entries(baseEntities)) {
|
|
624
|
+
merged[entityName] = stripRuntimeEntityLinks(entity);
|
|
625
|
+
}
|
|
626
|
+
for (const [entityName, entity] of Object.entries(nextEntities)) {
|
|
627
|
+
merged[entityName] =
|
|
628
|
+
entityName in merged
|
|
629
|
+
? mergeRuntimeEntityDefs(entityName, merged[entityName], entity)
|
|
630
|
+
: stripRuntimeEntityLinks(entity);
|
|
631
|
+
}
|
|
632
|
+
return merged;
|
|
633
|
+
}
|
|
501
634
|
function makeInstance(def, metaIncludes = []) {
|
|
502
635
|
const meta = {
|
|
503
636
|
name: def.name,
|
|
@@ -517,7 +650,7 @@ function makeInstance(def, metaIncludes = []) {
|
|
|
517
650
|
const otherDef = "schema" in other
|
|
518
651
|
? { entities: other.entities, links: other.links, rooms: other.rooms }
|
|
519
652
|
: other;
|
|
520
|
-
const mergedEntities =
|
|
653
|
+
const mergedEntities = mergeRuntimeEntities(def.entities, otherDef.entities);
|
|
521
654
|
const mergedLinks = { ...def.links, ...otherDef.links };
|
|
522
655
|
const mergedRooms = { ...def.rooms, ...otherDef.rooms };
|
|
523
656
|
const composed = makeInstance({
|
|
@@ -574,10 +707,10 @@ export function domain(arg) {
|
|
|
574
707
|
// Support lazy includes for circular dependencies by storing references and resolving at schema()/toInstantSchema() time
|
|
575
708
|
// AL preserves literal link keys from included domains
|
|
576
709
|
function createBuilder(deps, linkDeps, lazyIncludes = [], meta) {
|
|
577
|
-
|
|
710
|
+
const builder = {
|
|
578
711
|
includes(other) {
|
|
579
712
|
// Support lazy includes via function for circular dependencies
|
|
580
|
-
if (typeof other === 'function') {
|
|
713
|
+
if (typeof other === 'function' && !isMaterializedDomainSource(other)) {
|
|
581
714
|
const lazyGetter = () => {
|
|
582
715
|
try {
|
|
583
716
|
return other();
|
|
@@ -611,7 +744,7 @@ export function domain(arg) {
|
|
|
611
744
|
return createBuilder(deps, linkDeps, [...lazyIncludes, lazyGetter], nextMeta);
|
|
612
745
|
}
|
|
613
746
|
const links = other.links;
|
|
614
|
-
const mergedEntities =
|
|
747
|
+
const mergedEntities = mergeRuntimeEntities(deps, entities);
|
|
615
748
|
// Preserve literal link keys by merging directly (not casting to LinksDef)
|
|
616
749
|
const mergedLinks = (links ? { ...linkDeps, ...links } : { ...linkDeps });
|
|
617
750
|
const includeRef = () => other;
|
|
@@ -626,10 +759,11 @@ export function domain(arg) {
|
|
|
626
759
|
return createBuilder(deps, linkDeps, [...lazyIncludes, lazyGetter], nextMeta);
|
|
627
760
|
}
|
|
628
761
|
},
|
|
629
|
-
|
|
762
|
+
withSchema(def) {
|
|
630
763
|
// Resolve lazy includes at schema() time (when all domains should be initialized)
|
|
631
764
|
// This handles circular dependencies by deferring entity resolution
|
|
632
765
|
let resolvedDeps = { ...deps };
|
|
766
|
+
const pendingLazyIncludes = [];
|
|
633
767
|
// Preserve literal link keys from accumulated links
|
|
634
768
|
let resolvedLinks = { ...linkDeps };
|
|
635
769
|
for (const lazyGetter of lazyIncludes) {
|
|
@@ -638,7 +772,7 @@ export function domain(arg) {
|
|
|
638
772
|
if (other) {
|
|
639
773
|
const entities = other.entities;
|
|
640
774
|
if (entities) {
|
|
641
|
-
resolvedDeps =
|
|
775
|
+
resolvedDeps = mergeRuntimeEntities(resolvedDeps, entities);
|
|
642
776
|
}
|
|
643
777
|
const links = other.links;
|
|
644
778
|
if (links) {
|
|
@@ -646,14 +780,18 @@ export function domain(arg) {
|
|
|
646
780
|
resolvedLinks = { ...resolvedLinks, ...links };
|
|
647
781
|
}
|
|
648
782
|
}
|
|
783
|
+
else {
|
|
784
|
+
pendingLazyIncludes.push(lazyGetter);
|
|
785
|
+
}
|
|
649
786
|
}
|
|
650
787
|
catch (e) {
|
|
651
788
|
// If lazy resolution fails, continue - entities might be available via string references
|
|
652
789
|
// This is expected for circular dependencies that will be resolved when all domains are composed
|
|
790
|
+
pendingLazyIncludes.push(lazyGetter);
|
|
653
791
|
}
|
|
654
792
|
}
|
|
655
793
|
// Runtime merge for output; compile-time validation handled by types above
|
|
656
|
-
const allEntities =
|
|
794
|
+
const allEntities = mergeRuntimeEntities(resolvedDeps, def.entities);
|
|
657
795
|
// allLinks contains merged links from included domains + current domain
|
|
658
796
|
// Preserve literal link keys (owner, related, parent, etc.) by using MergeLinks
|
|
659
797
|
const allLinks = { ...resolvedLinks, ...def.links };
|
|
@@ -662,92 +800,112 @@ export function domain(arg) {
|
|
|
662
800
|
const capturedLinks = cloneLinksDef(allLinks);
|
|
663
801
|
const capturedRooms = cloneRoomsDef(def.rooms);
|
|
664
802
|
let cachedInstantSchema = null;
|
|
665
|
-
const
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
for (const lazyGetter of lazyIncludes) {
|
|
681
|
-
try {
|
|
682
|
-
const other = lazyGetter();
|
|
683
|
-
if (other) {
|
|
684
|
-
const entities = other.entities;
|
|
685
|
-
if (entities) {
|
|
686
|
-
finalEntities = { ...finalEntities, ...entities };
|
|
687
|
-
}
|
|
688
|
-
const links = other.links;
|
|
689
|
-
if (links) {
|
|
690
|
-
finalLinks = { ...finalLinks, ...links };
|
|
691
|
-
}
|
|
803
|
+
const instantSchema = () => {
|
|
804
|
+
if (cachedInstantSchema) {
|
|
805
|
+
return cachedInstantSchema;
|
|
806
|
+
}
|
|
807
|
+
let finalEntities = { ...capturedEntities };
|
|
808
|
+
let finalLinks = cloneLinksDef(capturedLinks);
|
|
809
|
+
let hasUnresolvedIncludes = false;
|
|
810
|
+
// Try to resolve lazy includes one more time (domains should be initialized by now)
|
|
811
|
+
for (const lazyGetter of pendingLazyIncludes) {
|
|
812
|
+
try {
|
|
813
|
+
const other = lazyGetter();
|
|
814
|
+
if (other) {
|
|
815
|
+
const entities = other.entities;
|
|
816
|
+
if (entities) {
|
|
817
|
+
finalEntities = mergeRuntimeEntities(finalEntities, entities);
|
|
692
818
|
}
|
|
693
|
-
|
|
694
|
-
|
|
819
|
+
const links = other.links;
|
|
820
|
+
if (links) {
|
|
821
|
+
finalLinks = { ...finalLinks, ...links };
|
|
695
822
|
}
|
|
696
823
|
}
|
|
697
|
-
|
|
698
|
-
// If still can't resolve, entities should already be in allEntities from app domain composition
|
|
824
|
+
else {
|
|
699
825
|
hasUnresolvedIncludes = true;
|
|
700
826
|
}
|
|
701
827
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
const baseEntities = {
|
|
706
|
-
$users: i.entity({
|
|
707
|
-
email: i.string().optional().indexed(),
|
|
708
|
-
}),
|
|
709
|
-
$files: i.entity({
|
|
710
|
-
path: i.string(),
|
|
711
|
-
url: i.string().optional(),
|
|
712
|
-
contentType: i.string().optional(),
|
|
713
|
-
size: i.number().optional(),
|
|
714
|
-
}),
|
|
715
|
-
$streams: i.entity({
|
|
716
|
-
clientId: i.string().optional().indexed(),
|
|
717
|
-
size: i.number().optional(),
|
|
718
|
-
createdAt: i.date().optional().indexed(),
|
|
719
|
-
updatedAt: i.date().optional().indexed(),
|
|
720
|
-
}),
|
|
721
|
-
};
|
|
722
|
-
// Merge base entities with user entities, user entities take precedence
|
|
723
|
-
const allEntitiesWithBase = {
|
|
724
|
-
...baseEntities,
|
|
725
|
-
...finalEntities,
|
|
726
|
-
};
|
|
727
|
-
const schemaResult = i.schema({
|
|
728
|
-
entities: allEntitiesWithBase,
|
|
729
|
-
links: cloneLinksDef(finalLinks),
|
|
730
|
-
rooms: cloneRoomsDef(capturedRooms),
|
|
731
|
-
});
|
|
732
|
-
const frozenSchema = Object.freeze(schemaResult);
|
|
733
|
-
if (!hasUnresolvedIncludes) {
|
|
734
|
-
cachedInstantSchema = frozenSchema;
|
|
828
|
+
catch {
|
|
829
|
+
// If still can't resolve, entities should already be in allEntities from app domain composition
|
|
830
|
+
hasUnresolvedIncludes = true;
|
|
735
831
|
}
|
|
736
|
-
|
|
737
|
-
|
|
832
|
+
}
|
|
833
|
+
assertNoDuplicateLinkAttributes(finalLinks);
|
|
834
|
+
// Include base entities ($users, $files, $streams) that InstantDB manages
|
|
835
|
+
// These need to be explicitly included since InstantDB doesn't auto-add them
|
|
836
|
+
const baseEntities = {
|
|
837
|
+
$users: i.entity({
|
|
838
|
+
email: i.string().optional().indexed(),
|
|
839
|
+
}),
|
|
840
|
+
$files: i.entity({
|
|
841
|
+
path: i.string(),
|
|
842
|
+
url: i.string().optional(),
|
|
843
|
+
contentType: i.string().optional(),
|
|
844
|
+
size: i.number().optional(),
|
|
845
|
+
}),
|
|
846
|
+
$streams: i.entity({
|
|
847
|
+
clientId: i.string().optional().indexed(),
|
|
848
|
+
size: i.number().optional(),
|
|
849
|
+
createdAt: i.date().optional().indexed(),
|
|
850
|
+
updatedAt: i.date().optional().indexed(),
|
|
851
|
+
}),
|
|
852
|
+
};
|
|
853
|
+
// Merge base entities with user entities, user entities take precedence
|
|
854
|
+
const allEntitiesWithBase = {
|
|
855
|
+
...baseEntities,
|
|
856
|
+
...finalEntities,
|
|
857
|
+
};
|
|
858
|
+
const schemaResult = i.schema({
|
|
859
|
+
entities: allEntitiesWithBase,
|
|
860
|
+
links: cloneLinksDef(finalLinks),
|
|
861
|
+
rooms: cloneRoomsDef(capturedRooms),
|
|
862
|
+
});
|
|
863
|
+
const frozenSchema = Object.freeze(schemaResult);
|
|
864
|
+
if (!hasUnresolvedIncludes) {
|
|
865
|
+
cachedInstantSchema = frozenSchema;
|
|
866
|
+
}
|
|
867
|
+
return frozenSchema;
|
|
738
868
|
};
|
|
869
|
+
let result;
|
|
870
|
+
const callableResult = (runtime, options) => callDomainRuntimeScope(result, runtime, options);
|
|
871
|
+
result = Object.assign(callableResult, {
|
|
872
|
+
entities: Object.freeze({ ...allEntities }),
|
|
873
|
+
// Strip base phantom from public type so it's assignable to i.schema()
|
|
874
|
+
links: Object.freeze(cloneLinksDef(allLinks)),
|
|
875
|
+
rooms: Object.freeze(cloneRoomsDef(def.rooms)),
|
|
876
|
+
// Add originalEntities for type-safe access to original entity definitions
|
|
877
|
+
originalEntities: Object.freeze({ ...allEntities }),
|
|
878
|
+
instantSchema,
|
|
879
|
+
toInstantSchema: instantSchema,
|
|
880
|
+
});
|
|
739
881
|
attachMeta(result, freezeMeta(meta));
|
|
740
882
|
result.context = (options) => buildContext(result, options);
|
|
741
883
|
result.contextString = (options) => contextToString(buildContext(result, options));
|
|
742
884
|
result.fromDB = (db, bindings) => createConcreteDomain(result, db, resolveSchema(result), bindings);
|
|
743
|
-
const
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
885
|
+
const reboundByAction = new Map();
|
|
886
|
+
const reboundActionMap = Object.fromEntries(Object.entries(seedActionMap).map(([key, action]) => {
|
|
887
|
+
const rebound = bindAction(action, {
|
|
888
|
+
name: action.name,
|
|
889
|
+
domain: result,
|
|
890
|
+
key,
|
|
891
|
+
});
|
|
892
|
+
reboundByAction.set(action, rebound);
|
|
893
|
+
return [key, rebound];
|
|
747
894
|
}));
|
|
895
|
+
const reboundActions = seedActions.map((action) => {
|
|
896
|
+
const rebound = reboundByAction.get(action);
|
|
897
|
+
if (rebound)
|
|
898
|
+
return rebound;
|
|
899
|
+
return bindAction(action, {
|
|
900
|
+
name: action.name,
|
|
901
|
+
domain: result,
|
|
902
|
+
key: getActionBinding(action)?.key,
|
|
903
|
+
});
|
|
904
|
+
});
|
|
748
905
|
setStoredActions(result, [...reboundActions]);
|
|
749
|
-
setStoredActionMap(result,
|
|
750
|
-
result.actions = (
|
|
906
|
+
setStoredActionMap(result, reboundActionMap);
|
|
907
|
+
result.actions = getStoredActionMap(result);
|
|
908
|
+
result.withActions = (actionsInput) => {
|
|
751
909
|
const current = getStoredActions(result);
|
|
752
910
|
const currentMap = getStoredActionMap(result);
|
|
753
911
|
const additions = normalizeActionCollection(result, actionsInput);
|
|
@@ -755,11 +913,16 @@ export function domain(arg) {
|
|
|
755
913
|
};
|
|
756
914
|
result.getActions = () => [...getStoredActions(result)];
|
|
757
915
|
result.getActionMap = () => ({ ...getStoredActionMap(result) });
|
|
916
|
+
result.definition = () => result;
|
|
758
917
|
return Object.freeze(result);
|
|
759
918
|
};
|
|
760
919
|
return createDomainResult([], {});
|
|
761
920
|
},
|
|
921
|
+
schema(def) {
|
|
922
|
+
return this.withSchema(def);
|
|
923
|
+
},
|
|
762
924
|
};
|
|
925
|
+
return builder;
|
|
763
926
|
}
|
|
764
927
|
if (typeof arg === "string" && !arg.trim()) {
|
|
765
928
|
throw new Error("domain() requires a name");
|
|
@@ -772,21 +935,34 @@ export function composeDomain(name, includes = []) {
|
|
|
772
935
|
for (const include of includes) {
|
|
773
936
|
builder = builder.includes(include);
|
|
774
937
|
}
|
|
775
|
-
return builder.
|
|
938
|
+
return builder.withSchema({ entities: {}, links: {}, rooms: {} });
|
|
776
939
|
}
|
|
777
940
|
/**
|
|
778
941
|
* Define a domain action without changing the public action contract.
|
|
779
942
|
*
|
|
780
943
|
* Convention for new actions:
|
|
781
944
|
*
|
|
782
|
-
* `async execute({ runtime, input }) {
|
|
945
|
+
* `async execute({ runtime, input }) { await runtime.db.transact([...]); }`
|
|
783
946
|
*
|
|
784
|
-
* Actions
|
|
785
|
-
* composition
|
|
947
|
+
* Actions receive a runtime already scoped to the declaring domain. Nested
|
|
948
|
+
* action composition is available through `runtime.actions.*`.
|
|
786
949
|
*/
|
|
950
|
+
function toJsonSchema(schema) {
|
|
951
|
+
try {
|
|
952
|
+
return z.toJSONSchema(schema, { target: "draft-7" });
|
|
953
|
+
}
|
|
954
|
+
catch {
|
|
955
|
+
return undefined;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
787
958
|
export function defineDomainAction(action) {
|
|
788
|
-
return
|
|
959
|
+
return Object.freeze({
|
|
960
|
+
...action,
|
|
961
|
+
inputSchema: toJsonSchema(action.input),
|
|
962
|
+
outputSchema: toJsonSchema(action.output),
|
|
963
|
+
});
|
|
789
964
|
}
|
|
965
|
+
export const defineAction = defineDomainAction;
|
|
790
966
|
export function getDomainActions(source) {
|
|
791
967
|
return getStoredActions(source);
|
|
792
968
|
}
|