@objectstack/objectql 7.2.1 → 7.4.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/dist/index.d.mts +343 -9
- package/dist/index.d.ts +343 -9
- package/dist/index.js +606 -23
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +599 -25
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -29,6 +29,8 @@ __export(index_exports, {
|
|
|
29
29
|
ObjectRepository: () => ObjectRepository,
|
|
30
30
|
ObjectStackProtocolImplementation: () => ObjectStackProtocolImplementation,
|
|
31
31
|
RESERVED_NAMESPACES: () => RESERVED_NAMESPACES,
|
|
32
|
+
SECRET_MASK: () => SECRET_MASK,
|
|
33
|
+
SECRET_REF_PREFIX: () => SECRET_REF_PREFIX,
|
|
32
34
|
SchemaRegistry: () => SchemaRegistry,
|
|
33
35
|
ScopedContext: () => ScopedContext,
|
|
34
36
|
SysMetadataRepository: () => SysMetadataRepository,
|
|
@@ -37,11 +39,18 @@ __export(index_exports, {
|
|
|
37
39
|
applySystemFields: () => applySystemFields,
|
|
38
40
|
bindHooksToEngine: () => bindHooksToEngine,
|
|
39
41
|
bucketDateValue: () => bucketDateValue,
|
|
42
|
+
collectSecretFields: () => collectSecretFields,
|
|
40
43
|
computeFQN: () => computeFQN,
|
|
41
44
|
convertIntrospectedSchemaToObjects: () => convertIntrospectedSchemaToObjects,
|
|
42
45
|
createObjectQLKernel: () => createObjectQLKernel,
|
|
46
|
+
evaluateValidationRules: () => evaluateValidationRules,
|
|
47
|
+
isSecretRef: () => isSecretRef,
|
|
48
|
+
legalNextStates: () => legalNextStates,
|
|
49
|
+
makeSecretRef: () => makeSecretRef,
|
|
50
|
+
needsPriorRecord: () => needsPriorRecord,
|
|
43
51
|
noopHookMetricsRecorder: () => noopHookMetricsRecorder,
|
|
44
52
|
parseFQN: () => parseFQN,
|
|
53
|
+
parseSecretRef: () => parseSecretRef,
|
|
45
54
|
toTitleCase: () => toTitleCase,
|
|
46
55
|
validateRecord: () => validateRecord,
|
|
47
56
|
wrapDeclarativeHook: () => wrapDeclarativeHook
|
|
@@ -178,6 +187,23 @@ var SchemaRegistry = class {
|
|
|
178
187
|
// ==========================================
|
|
179
188
|
/** Type → Name/ID → MetadataItem */
|
|
180
189
|
this.metadata = /* @__PURE__ */ new Map();
|
|
190
|
+
/**
|
|
191
|
+
* App name → navigation contributions (ADR-0029 D7).
|
|
192
|
+
*
|
|
193
|
+
* Lets packages inject nav items into apps they do not own (the UI analog
|
|
194
|
+
* of object extenders). Merged into the owning app's `navigation` tree on
|
|
195
|
+
* read in {@link getApp} / {@link getAllApps} by group id + priority.
|
|
196
|
+
*/
|
|
197
|
+
this.appNavContributions = /* @__PURE__ */ new Map();
|
|
198
|
+
/**
|
|
199
|
+
* Package ids that must be installed in a DISABLED state. Seeded once at
|
|
200
|
+
* boot (from persisted state) BEFORE any package registration so that every
|
|
201
|
+
* registration path — boot artifact, marketplace rehydrate, local import —
|
|
202
|
+
* honors persisted disable state uniformly without a fragile post-boot
|
|
203
|
+
* re-application hook. See {@link setInitialDisabledPackageIds} and
|
|
204
|
+
* {@link installPackage}.
|
|
205
|
+
*/
|
|
206
|
+
this.initialDisabledPackageIds = /* @__PURE__ */ new Set();
|
|
181
207
|
if (options.multiTenant !== void 0) {
|
|
182
208
|
this.multiTenant = options.multiTenant;
|
|
183
209
|
} else {
|
|
@@ -194,6 +220,14 @@ var SchemaRegistry = class {
|
|
|
194
220
|
if (this._logLevel === "silent" || this._logLevel === "error" || this._logLevel === "warn") return;
|
|
195
221
|
console.log(msg);
|
|
196
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* Seed the set of package ids that should be installed disabled. Call this
|
|
225
|
+
* before package registration begins; later `installPackage` calls for these
|
|
226
|
+
* ids land in the `disabled` state. Replaces any previously seeded set.
|
|
227
|
+
*/
|
|
228
|
+
setInitialDisabledPackageIds(ids) {
|
|
229
|
+
this.initialDisabledPackageIds = new Set(ids);
|
|
230
|
+
}
|
|
197
231
|
// ==========================================
|
|
198
232
|
// Namespace Management
|
|
199
233
|
// ==========================================
|
|
@@ -388,6 +422,47 @@ var SchemaRegistry = class {
|
|
|
388
422
|
const contributors = this.objectContributors.get(fqn);
|
|
389
423
|
return contributors?.find((c) => c.ownership === "own");
|
|
390
424
|
}
|
|
425
|
+
/**
|
|
426
|
+
* ADR-0029 K0 — assert every registered object resolves to exactly one
|
|
427
|
+
* owner.
|
|
428
|
+
*
|
|
429
|
+
* A second `own` from a different package is already rejected eagerly in
|
|
430
|
+
* {@link registerObject} (it throws). This is the install-time backstop
|
|
431
|
+
* called once all packages are registered (kernel bootstrap complete),
|
|
432
|
+
* and it additionally catches the case `registerObject` cannot: an object
|
|
433
|
+
* that has only `extend` contributions and **no owner** — which would
|
|
434
|
+
* otherwise resolve to nothing. Surfacing it here turns a silent
|
|
435
|
+
* "extend a non-existent object" into a clear bootstrap error.
|
|
436
|
+
*
|
|
437
|
+
* This is the invariant the kernel-decomposition (ADR-0029) relies on:
|
|
438
|
+
* the `sys` namespace is shared across many first-party plugins, but each
|
|
439
|
+
* object name has exactly one owner.
|
|
440
|
+
*
|
|
441
|
+
* @throws Error listing every object whose owner count is not exactly 1.
|
|
442
|
+
*/
|
|
443
|
+
assertSingleOwnerPerObject() {
|
|
444
|
+
const violations = [];
|
|
445
|
+
for (const [fqn, contributors] of this.objectContributors.entries()) {
|
|
446
|
+
const owners = contributors.filter((c) => c.ownership === "own");
|
|
447
|
+
if (owners.length === 0) {
|
|
448
|
+
const extenders = contributors.map((c) => c.packageId).join(", ") || "(none)";
|
|
449
|
+
violations.push(
|
|
450
|
+
`Object "${fqn}" has no owner \u2014 only extend contributions from [${extenders}]. Exactly one package must register it with ownership 'own'.`
|
|
451
|
+
);
|
|
452
|
+
} else if (owners.length > 1) {
|
|
453
|
+
const names = owners.map((c) => c.packageId).join(", ");
|
|
454
|
+
violations.push(
|
|
455
|
+
`Object "${fqn}" has ${owners.length} owners [${names}] \u2014 exactly one is allowed.`
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if (violations.length > 0) {
|
|
460
|
+
throw new Error(
|
|
461
|
+
`[Registry] single-owner-per-object check failed (ADR-0029):
|
|
462
|
+
` + violations.join("\n ")
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
391
466
|
/**
|
|
392
467
|
* Unregister all objects contributed by a package.
|
|
393
468
|
*
|
|
@@ -519,10 +594,23 @@ var SchemaRegistry = class {
|
|
|
519
594
|
return this.getAllObjects(packageId);
|
|
520
595
|
}
|
|
521
596
|
const items = Array.from(this.metadata.get(type)?.values() || []);
|
|
597
|
+
let result = items;
|
|
522
598
|
if (packageId) {
|
|
523
|
-
|
|
599
|
+
result = result.filter((item) => item._packageId === packageId);
|
|
524
600
|
}
|
|
525
|
-
|
|
601
|
+
if (type !== "package") {
|
|
602
|
+
result = result.filter((item) => !this.isPackageDisabled(item?._packageId));
|
|
603
|
+
}
|
|
604
|
+
return result;
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Whether a package has been explicitly disabled. Unknown packages and
|
|
608
|
+
* items with no owning package are treated as enabled.
|
|
609
|
+
*/
|
|
610
|
+
isPackageDisabled(packageId) {
|
|
611
|
+
if (!packageId) return false;
|
|
612
|
+
const pkg = this.getPackage(packageId);
|
|
613
|
+
return pkg?.enabled === false || pkg?.status === "disabled";
|
|
526
614
|
}
|
|
527
615
|
/**
|
|
528
616
|
* Get all registered metadata types (Kinds)
|
|
@@ -539,12 +627,14 @@ var SchemaRegistry = class {
|
|
|
539
627
|
// ==========================================
|
|
540
628
|
installPackage(manifest, settings) {
|
|
541
629
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
630
|
+
const disabled = this.initialDisabledPackageIds.has(manifest.id);
|
|
542
631
|
const pkg = {
|
|
543
632
|
manifest,
|
|
544
|
-
status: "installed",
|
|
545
|
-
enabled:
|
|
633
|
+
status: disabled ? "disabled" : "installed",
|
|
634
|
+
enabled: !disabled,
|
|
546
635
|
installedAt: now,
|
|
547
636
|
updatedAt: now,
|
|
637
|
+
...disabled ? { statusChangedAt: now } : {},
|
|
548
638
|
settings
|
|
549
639
|
};
|
|
550
640
|
if (manifest.namespace) {
|
|
@@ -614,10 +704,88 @@ var SchemaRegistry = class {
|
|
|
614
704
|
this.registerItem("app", app, "name", packageId);
|
|
615
705
|
}
|
|
616
706
|
getApp(name) {
|
|
617
|
-
|
|
707
|
+
const app = this.getItem("app", name);
|
|
708
|
+
if (!app) return app;
|
|
709
|
+
return this.applyNavContributions(app);
|
|
618
710
|
}
|
|
619
711
|
getAllApps() {
|
|
620
|
-
return this.listItems("app");
|
|
712
|
+
return this.listItems("app").map((app) => this.applyNavContributions(app));
|
|
713
|
+
}
|
|
714
|
+
// ==========================================
|
|
715
|
+
// App navigation contributions (ADR-0029 D7)
|
|
716
|
+
// ==========================================
|
|
717
|
+
/**
|
|
718
|
+
* Register a navigation contribution — a package injecting nav items into
|
|
719
|
+
* an app it does not own (the UI-layer analog of object `extend`).
|
|
720
|
+
*
|
|
721
|
+
* Contributions are merged into the target app's `navigation` tree lazily
|
|
722
|
+
* on read ({@link getApp} / {@link getAllApps}) by group id + priority, so
|
|
723
|
+
* registration order does not matter and the owning app can be registered
|
|
724
|
+
* before or after its contributors.
|
|
725
|
+
*/
|
|
726
|
+
registerAppNavContribution(contribution, packageId) {
|
|
727
|
+
if (!contribution || !contribution.app) return;
|
|
728
|
+
const list = this.appNavContributions.get(contribution.app) ?? [];
|
|
729
|
+
list.push({
|
|
730
|
+
packageId,
|
|
731
|
+
group: contribution.group,
|
|
732
|
+
priority: contribution.priority ?? 200,
|
|
733
|
+
items: Array.isArray(contribution.items) ? contribution.items : []
|
|
734
|
+
});
|
|
735
|
+
this.appNavContributions.set(contribution.app, list);
|
|
736
|
+
this.log(
|
|
737
|
+
`[Registry] Navigation contribution: ${packageId ?? "(unknown)"} -> ${contribution.app}` + (contribution.group ? `/${contribution.group}` : "") + ` (${list[list.length - 1].items.length} items)`
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
/** Contributions registered for an app (empty array when none). */
|
|
741
|
+
getAppNavContributions(appName) {
|
|
742
|
+
return this.appNavContributions.get(appName) ?? [];
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Return a copy of `app` with all registered navigation contributions
|
|
746
|
+
* merged into its `navigation` tree. The stored app is never mutated, so
|
|
747
|
+
* repeated reads stay idempotent.
|
|
748
|
+
*
|
|
749
|
+
* Public so the protocol serving path (`getMetaItems` / `getMetaItem` for
|
|
750
|
+
* `app`) can merge contributions the same way `getApp` / `getAllApps` do —
|
|
751
|
+
* the REST app endpoints read through the protocol, not these helpers, so
|
|
752
|
+
* the merge must be reachable from there too (ADR-0029 D7).
|
|
753
|
+
*/
|
|
754
|
+
applyNavContributions(app) {
|
|
755
|
+
const contributions = this.appNavContributions.get(app?.name);
|
|
756
|
+
if (!contributions || contributions.length === 0) return app;
|
|
757
|
+
const cloned = structuredClone(app);
|
|
758
|
+
const nav = Array.isArray(cloned.navigation) ? cloned.navigation : cloned.navigation = [];
|
|
759
|
+
const sorted = [...contributions].sort((a, b) => a.priority - b.priority);
|
|
760
|
+
for (const c of sorted) {
|
|
761
|
+
if (!c.items.length) continue;
|
|
762
|
+
if (c.group) {
|
|
763
|
+
const group = this.findNavGroup(nav, c.group);
|
|
764
|
+
if (group) {
|
|
765
|
+
if (!Array.isArray(group.children)) group.children = [];
|
|
766
|
+
group.children.push(...c.items);
|
|
767
|
+
} else {
|
|
768
|
+
this.log(
|
|
769
|
+
`[Registry] Navigation contribution from "${c.packageId ?? "(unknown)"}" targets missing group "${c.group}" in app "${app.name}" \u2014 appending at top level.`
|
|
770
|
+
);
|
|
771
|
+
nav.push(...c.items);
|
|
772
|
+
}
|
|
773
|
+
} else {
|
|
774
|
+
nav.push(...c.items);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
return cloned;
|
|
778
|
+
}
|
|
779
|
+
/** Depth-first search for a `type: 'group'` nav item by id. */
|
|
780
|
+
findNavGroup(items, groupId) {
|
|
781
|
+
for (const item of items) {
|
|
782
|
+
if (item && item.id === groupId && item.type === "group") return item;
|
|
783
|
+
if (item && Array.isArray(item.children)) {
|
|
784
|
+
const found = this.findNavGroup(item.children, groupId);
|
|
785
|
+
if (found) return found;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
return void 0;
|
|
621
789
|
}
|
|
622
790
|
// ==========================================
|
|
623
791
|
// Plugin Helpers
|
|
@@ -676,6 +844,7 @@ var SchemaRegistry = class {
|
|
|
676
844
|
this.mergedObjectCache.clear();
|
|
677
845
|
this.namespaceRegistry.clear();
|
|
678
846
|
this.metadata.clear();
|
|
847
|
+
this.appNavContributions.clear();
|
|
679
848
|
this.log("[Registry] Reset complete");
|
|
680
849
|
}
|
|
681
850
|
};
|
|
@@ -830,6 +999,12 @@ var SysMetadataRepository = class {
|
|
|
830
999
|
version,
|
|
831
1000
|
updated_at: now
|
|
832
1001
|
};
|
|
1002
|
+
if (existing) {
|
|
1003
|
+
const existingPkg = existing.package_id ?? null;
|
|
1004
|
+
parentRowData.package_id = existingPkg ?? opts.packageId ?? null;
|
|
1005
|
+
} else {
|
|
1006
|
+
parentRowData.package_id = opts.packageId ?? null;
|
|
1007
|
+
}
|
|
833
1008
|
if (existing) {
|
|
834
1009
|
const existingId = existing.id;
|
|
835
1010
|
if (existingId === void 0) {
|
|
@@ -1361,6 +1536,7 @@ var SysMetadataRepository = class {
|
|
|
1361
1536
|
var import_metadata_core2 = require("@objectstack/metadata-core");
|
|
1362
1537
|
var import_data2 = require("@objectstack/spec/data");
|
|
1363
1538
|
var import_shared4 = require("@objectstack/spec/shared");
|
|
1539
|
+
var import_ui2 = require("@objectstack/spec/ui");
|
|
1364
1540
|
var import_system = require("@objectstack/spec/system");
|
|
1365
1541
|
var import_kernel4 = require("@objectstack/spec/kernel");
|
|
1366
1542
|
var import_kernel5 = require("@objectstack/spec/kernel");
|
|
@@ -2055,6 +2231,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2055
2231
|
const zodSchema = (0, import_kernel4.getMetadataTypeSchema)(singular);
|
|
2056
2232
|
const schema = (zodSchema ? toJsonSchemaSafe(zodSchema) : void 0) ?? HAND_CRAFTED_SCHEMAS[singular];
|
|
2057
2233
|
const form = TYPE_TO_FORM[singular];
|
|
2234
|
+
const typeActions = (0, import_kernel4.getMetadataTypeActions)(singular);
|
|
2058
2235
|
const base = registryByType.get(singular);
|
|
2059
2236
|
if (base) {
|
|
2060
2237
|
const isEnvOverridden = writableOverrides.has(singular);
|
|
@@ -2066,7 +2243,11 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2066
2243
|
allowOrgOverride: base.allowOrgOverride || isEnvOverridden,
|
|
2067
2244
|
overrideSource: isEnvOverridden && !base.allowOrgOverride ? "env" : "registry",
|
|
2068
2245
|
schema,
|
|
2069
|
-
form
|
|
2246
|
+
form,
|
|
2247
|
+
// Override the spread `base.actions` with the merged view
|
|
2248
|
+
// (declarative + plugin-registered). Omit when empty to
|
|
2249
|
+
// preserve the prior "no actions key" response shape.
|
|
2250
|
+
...typeActions.length ? { actions: typeActions } : {}
|
|
2070
2251
|
};
|
|
2071
2252
|
}
|
|
2072
2253
|
return {
|
|
@@ -2085,7 +2266,9 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2085
2266
|
domain: "system",
|
|
2086
2267
|
overrideSource: writableOverrides.has(singular) ? "env" : "registry",
|
|
2087
2268
|
schema,
|
|
2088
|
-
form
|
|
2269
|
+
form,
|
|
2270
|
+
// Plugin-registered actions on a type with no registry entry.
|
|
2271
|
+
...typeActions.length ? { actions: typeActions } : {}
|
|
2089
2272
|
};
|
|
2090
2273
|
}).sort((a, b) => {
|
|
2091
2274
|
if (a.domain !== b.domain) return a.domain.localeCompare(b.domain);
|
|
@@ -2183,13 +2366,13 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2183
2366
|
state: "active",
|
|
2184
2367
|
organization_id: oid
|
|
2185
2368
|
};
|
|
2186
|
-
if (packageId) whereClause.
|
|
2369
|
+
if (packageId) whereClause.package_id = packageId;
|
|
2187
2370
|
let rs = await this.engine.find("sys_metadata", { where: whereClause });
|
|
2188
2371
|
if (!rs || rs.length === 0) {
|
|
2189
2372
|
const alt = import_shared4.PLURAL_TO_SINGULAR[request.type] ?? import_shared4.SINGULAR_TO_PLURAL[request.type];
|
|
2190
2373
|
if (alt) {
|
|
2191
2374
|
const altWhere = { type: alt, state: "active", organization_id: oid };
|
|
2192
|
-
if (packageId) altWhere.
|
|
2375
|
+
if (packageId) altWhere.package_id = packageId;
|
|
2193
2376
|
rs = await this.engine.find("sys_metadata", { where: altWhere });
|
|
2194
2377
|
}
|
|
2195
2378
|
}
|
|
@@ -2212,6 +2395,10 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2212
2395
|
for (const record of records) {
|
|
2213
2396
|
const data = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
|
|
2214
2397
|
if (data && typeof data === "object" && "name" in data) {
|
|
2398
|
+
const recPkg = record.package_id ?? void 0;
|
|
2399
|
+
if (recPkg && data._packageId === void 0) {
|
|
2400
|
+
data._packageId = recPkg;
|
|
2401
|
+
}
|
|
2215
2402
|
byName.set(data.name, data);
|
|
2216
2403
|
}
|
|
2217
2404
|
if (this.environmentId === void 0) {
|
|
@@ -2251,6 +2438,17 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2251
2438
|
}
|
|
2252
2439
|
} catch {
|
|
2253
2440
|
}
|
|
2441
|
+
if (request.type !== "package" && request.type !== "object" && request.type !== "objects") {
|
|
2442
|
+
items = items.filter(
|
|
2443
|
+
(it) => !this.engine.registry.isPackageDisabled(it?._packageId)
|
|
2444
|
+
);
|
|
2445
|
+
}
|
|
2446
|
+
if (request.type === "view" || request.type === "views") {
|
|
2447
|
+
items = items.filter((it) => !(0, import_ui2.isAggregatedViewContainer)(it));
|
|
2448
|
+
}
|
|
2449
|
+
if (request.type === "app" || request.type === "apps") {
|
|
2450
|
+
items = items.map((app) => this.engine.registry.applyNavContributions(app));
|
|
2451
|
+
}
|
|
2254
2452
|
return {
|
|
2255
2453
|
type: request.type,
|
|
2256
2454
|
items: decorateMetadataItems(
|
|
@@ -2294,6 +2492,10 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2294
2492
|
const record = (orgId ? await findOverlay(orgId) : void 0) ?? await findOverlay(null);
|
|
2295
2493
|
if (record) {
|
|
2296
2494
|
item = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
|
|
2495
|
+
const recPkg = record.package_id ?? void 0;
|
|
2496
|
+
if (recPkg && item && typeof item === "object" && item._packageId === void 0) {
|
|
2497
|
+
item._packageId = recPkg;
|
|
2498
|
+
}
|
|
2297
2499
|
}
|
|
2298
2500
|
} catch {
|
|
2299
2501
|
}
|
|
@@ -2336,6 +2538,9 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2336
2538
|
if (alt) item = this.engine.registry.getItem(alt, request.name);
|
|
2337
2539
|
}
|
|
2338
2540
|
}
|
|
2541
|
+
if ((request.type === "app" || request.type === "apps") && item) {
|
|
2542
|
+
item = this.engine.registry.applyNavContributions(item);
|
|
2543
|
+
}
|
|
2339
2544
|
const artifactItem = this.lookupArtifactItem(request.type, request.name);
|
|
2340
2545
|
const decorated = decorateMetadataItem(
|
|
2341
2546
|
request.type,
|
|
@@ -3094,7 +3299,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3094
3299
|
throw new Error(`Metadata item ${request.type}/${request.name} not found`);
|
|
3095
3300
|
}
|
|
3096
3301
|
const content = JSON.stringify(item);
|
|
3097
|
-
const hash = simpleHash(content);
|
|
3302
|
+
const hash = simpleHash(request.locale ? `${request.locale}\0${content}` : content);
|
|
3098
3303
|
const etag = { value: hash, weak: false };
|
|
3099
3304
|
if (request.cacheRequest?.ifNoneMatch) {
|
|
3100
3305
|
const clientEtag = request.cacheRequest.ifNoneMatch.replace(/^"(.*)"$/, "$1").replace(/^W\/"(.*)"$/, "$1");
|
|
@@ -3740,7 +3945,8 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3740
3945
|
actor: request.actor ?? "system",
|
|
3741
3946
|
source: "protocol.saveMetaItem",
|
|
3742
3947
|
intent,
|
|
3743
|
-
state: mode === "draft" ? "draft" : "active"
|
|
3948
|
+
state: mode === "draft" ? "draft" : "active",
|
|
3949
|
+
...request.packageId !== void 0 ? { packageId: request.packageId } : {}
|
|
3744
3950
|
});
|
|
3745
3951
|
if (mode === "publish") {
|
|
3746
3952
|
this.applyObjectRegistryMutation(request);
|
|
@@ -3791,12 +3997,16 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3791
3997
|
where: scopedWhere
|
|
3792
3998
|
});
|
|
3793
3999
|
if (existing) {
|
|
3794
|
-
|
|
4000
|
+
const updateRow = {
|
|
3795
4001
|
metadata: JSON.stringify(request.item),
|
|
3796
4002
|
updated_at: now,
|
|
3797
4003
|
version: (existing.version || 0) + 1,
|
|
3798
4004
|
state: "active"
|
|
3799
|
-
}
|
|
4005
|
+
};
|
|
4006
|
+
const existingPkg = existing.package_id ?? null;
|
|
4007
|
+
const nextPkg = existingPkg ?? request.packageId ?? null;
|
|
4008
|
+
if (nextPkg !== null) updateRow.package_id = nextPkg;
|
|
4009
|
+
await this.engine.update("sys_metadata", updateRow, {
|
|
3800
4010
|
where: { id: existing.id }
|
|
3801
4011
|
});
|
|
3802
4012
|
} else {
|
|
@@ -3816,6 +4026,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3816
4026
|
updated_at: now,
|
|
3817
4027
|
organization_id: orgId
|
|
3818
4028
|
};
|
|
4029
|
+
if (request.packageId) row.package_id = request.packageId;
|
|
3819
4030
|
await this.engine.insert("sys_metadata", row);
|
|
3820
4031
|
}
|
|
3821
4032
|
return {
|
|
@@ -4540,8 +4751,33 @@ var ObjectStackProtocolImplementation = _ObjectStackProtocolImplementation;
|
|
|
4540
4751
|
var import_kernel6 = require("@objectstack/spec/kernel");
|
|
4541
4752
|
var import_core = require("@objectstack/core");
|
|
4542
4753
|
var import_system2 = require("@objectstack/spec/system");
|
|
4754
|
+
|
|
4755
|
+
// src/secret-fields.ts
|
|
4756
|
+
var SECRET_REF_PREFIX = "secret:";
|
|
4757
|
+
var SECRET_MASK = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
|
|
4758
|
+
function makeSecretRef(handleId) {
|
|
4759
|
+
return `${SECRET_REF_PREFIX}${handleId}`;
|
|
4760
|
+
}
|
|
4761
|
+
function isSecretRef(value) {
|
|
4762
|
+
return typeof value === "string" && value.startsWith(SECRET_REF_PREFIX);
|
|
4763
|
+
}
|
|
4764
|
+
function parseSecretRef(value) {
|
|
4765
|
+
return isSecretRef(value) ? value.slice(SECRET_REF_PREFIX.length) : null;
|
|
4766
|
+
}
|
|
4767
|
+
function collectSecretFields(schema) {
|
|
4768
|
+
const fields = schema?.fields;
|
|
4769
|
+
if (!fields) return [];
|
|
4770
|
+
const out = [];
|
|
4771
|
+
for (const [name, def] of Object.entries(fields)) {
|
|
4772
|
+
if (def && def.type === "secret") out.push(name);
|
|
4773
|
+
}
|
|
4774
|
+
return out;
|
|
4775
|
+
}
|
|
4776
|
+
|
|
4777
|
+
// src/engine.ts
|
|
4543
4778
|
var import_shared5 = require("@objectstack/spec/shared");
|
|
4544
|
-
var
|
|
4779
|
+
var import_formula3 = require("@objectstack/formula");
|
|
4780
|
+
var import_spec = require("@objectstack/spec");
|
|
4545
4781
|
|
|
4546
4782
|
// src/hook-wrappers.ts
|
|
4547
4783
|
var import_formula = require("@objectstack/formula");
|
|
@@ -5095,6 +5331,101 @@ function validateRecord(objectSchema, data, mode) {
|
|
|
5095
5331
|
if (errors.length > 0) throw new ValidationError(errors);
|
|
5096
5332
|
}
|
|
5097
5333
|
|
|
5334
|
+
// src/validation/rule-validator.ts
|
|
5335
|
+
var import_formula2 = require("@objectstack/formula");
|
|
5336
|
+
function needsPriorRecord(objectSchema) {
|
|
5337
|
+
const rules = objectSchema?.validations;
|
|
5338
|
+
if (!Array.isArray(rules)) return false;
|
|
5339
|
+
return rules.some(
|
|
5340
|
+
(r) => r != null && typeof r === "object" && (r.type === "state_machine" || r.type === "cross_field" || r.type === "script")
|
|
5341
|
+
);
|
|
5342
|
+
}
|
|
5343
|
+
function toExpression(cond) {
|
|
5344
|
+
return typeof cond === "string" ? { dialect: "cel", source: cond } : cond;
|
|
5345
|
+
}
|
|
5346
|
+
function evaluateValidationRules(objectSchema, data, mode, opts = {}) {
|
|
5347
|
+
const rules = objectSchema?.validations;
|
|
5348
|
+
if (!Array.isArray(rules) || rules.length === 0 || !data) return;
|
|
5349
|
+
const previous = opts.previous ?? void 0;
|
|
5350
|
+
const merged = { ...previous ?? {}, ...data };
|
|
5351
|
+
const errors = [];
|
|
5352
|
+
const ordered = rules.filter((r) => r != null && typeof r === "object").filter((r) => r.active !== false).filter((r) => {
|
|
5353
|
+
const events = r.events ?? ["insert", "update"];
|
|
5354
|
+
return events.includes(mode);
|
|
5355
|
+
}).sort((a, b) => (a.priority ?? 100) - (b.priority ?? 100));
|
|
5356
|
+
for (const rule of ordered) {
|
|
5357
|
+
let violation = null;
|
|
5358
|
+
try {
|
|
5359
|
+
if (rule.type === "state_machine") {
|
|
5360
|
+
violation = checkStateMachine(rule, mode, data, previous);
|
|
5361
|
+
} else if (rule.type === "script" || rule.type === "cross_field") {
|
|
5362
|
+
violation = checkPredicate(rule, merged, previous, opts.logger);
|
|
5363
|
+
}
|
|
5364
|
+
} catch (err) {
|
|
5365
|
+
opts.logger?.warn?.(`Validation rule '${rule.name}' threw \u2014 skipped`, err);
|
|
5366
|
+
continue;
|
|
5367
|
+
}
|
|
5368
|
+
if (!violation) continue;
|
|
5369
|
+
const severity = rule.severity ?? "error";
|
|
5370
|
+
if (severity === "error") {
|
|
5371
|
+
errors.push(violation);
|
|
5372
|
+
} else {
|
|
5373
|
+
opts.logger?.warn?.(
|
|
5374
|
+
`Validation rule '${rule.name}' (${severity}): ${violation.message}`
|
|
5375
|
+
);
|
|
5376
|
+
}
|
|
5377
|
+
}
|
|
5378
|
+
if (errors.length > 0) throw new ValidationError(errors);
|
|
5379
|
+
}
|
|
5380
|
+
function checkStateMachine(rule, mode, data, previous) {
|
|
5381
|
+
if (mode === "insert" || !previous) return null;
|
|
5382
|
+
if (!(rule.field in data)) return null;
|
|
5383
|
+
const from = previous[rule.field];
|
|
5384
|
+
const to = data[rule.field];
|
|
5385
|
+
if (from === to || to === void 0 || to === null) return null;
|
|
5386
|
+
const fromKey = String(from);
|
|
5387
|
+
const allowed = rule.transitions[fromKey];
|
|
5388
|
+
if (!Array.isArray(allowed)) return null;
|
|
5389
|
+
if (!allowed.includes(String(to))) {
|
|
5390
|
+
return {
|
|
5391
|
+
field: rule.field,
|
|
5392
|
+
code: "invalid_transition",
|
|
5393
|
+
message: rule.message || `Invalid transition for ${rule.field}: ${fromKey} \u2192 ${String(to)}`
|
|
5394
|
+
};
|
|
5395
|
+
}
|
|
5396
|
+
return null;
|
|
5397
|
+
}
|
|
5398
|
+
function checkPredicate(rule, record, previous, logger) {
|
|
5399
|
+
const expr = toExpression(rule.condition);
|
|
5400
|
+
const result = import_formula2.ExpressionEngine.evaluate(expr, {
|
|
5401
|
+
record,
|
|
5402
|
+
previous: previous ?? void 0
|
|
5403
|
+
});
|
|
5404
|
+
if (!result.ok) {
|
|
5405
|
+
logger?.warn?.(
|
|
5406
|
+
`Validation rule '${rule.name}' predicate failed to evaluate (${result.error.kind}: ${result.error.message}) \u2014 skipped`
|
|
5407
|
+
);
|
|
5408
|
+
return null;
|
|
5409
|
+
}
|
|
5410
|
+
if (result.value === true) {
|
|
5411
|
+
return {
|
|
5412
|
+
field: rule.fields?.[0] ?? "_record",
|
|
5413
|
+
code: "rule_violation",
|
|
5414
|
+
message: rule.message
|
|
5415
|
+
};
|
|
5416
|
+
}
|
|
5417
|
+
return null;
|
|
5418
|
+
}
|
|
5419
|
+
function legalNextStates(objectSchema, field, currentState) {
|
|
5420
|
+
const rules = objectSchema?.validations;
|
|
5421
|
+
if (!Array.isArray(rules)) return null;
|
|
5422
|
+
const rule = rules.find(
|
|
5423
|
+
(r) => r != null && typeof r === "object" && r.type === "state_machine" && r.field === field
|
|
5424
|
+
);
|
|
5425
|
+
if (!rule) return null;
|
|
5426
|
+
return rule.transitions[currentState] ?? [];
|
|
5427
|
+
}
|
|
5428
|
+
|
|
5098
5429
|
// src/in-memory-aggregation.ts
|
|
5099
5430
|
function applyInMemoryAggregation(rows, ast) {
|
|
5100
5431
|
const groupBy = ast.groupBy ?? [];
|
|
@@ -5252,7 +5583,7 @@ function planFormulaProjection(schema, requestedFields) {
|
|
|
5252
5583
|
if (def?.type === "formula" && def.expression) {
|
|
5253
5584
|
const expr = typeof def.expression === "string" ? { dialect: "cel", source: def.expression } : def.expression;
|
|
5254
5585
|
plan.push({ name: f, expression: expr });
|
|
5255
|
-
|
|
5586
|
+
import_formula3.ExpressionEngine.compile(expr);
|
|
5256
5587
|
} else if (Array.isArray(requestedFields) && requestedFields.length > 0) {
|
|
5257
5588
|
projected.add(f);
|
|
5258
5589
|
}
|
|
@@ -5274,7 +5605,7 @@ function applyFormulaPlan(plan, records) {
|
|
|
5274
5605
|
for (const rec of records) {
|
|
5275
5606
|
if (rec == null) continue;
|
|
5276
5607
|
for (const fp of plan) {
|
|
5277
|
-
const r =
|
|
5608
|
+
const r = import_formula3.ExpressionEngine.evaluate(fp.expression, { record: rec });
|
|
5278
5609
|
rec[fp.name] = r.ok ? r.value : null;
|
|
5279
5610
|
}
|
|
5280
5611
|
}
|
|
@@ -5284,7 +5615,7 @@ function resolveMetadataItemName(key, item) {
|
|
|
5284
5615
|
if (item.name) return item.name;
|
|
5285
5616
|
if (item.id) return item.id;
|
|
5286
5617
|
if (key === "views") {
|
|
5287
|
-
return item?.list?.data?.object || item?.form?.data?.object || void 0;
|
|
5618
|
+
return item?.object || item?.list?.data?.object || item?.form?.data?.object || void 0;
|
|
5288
5619
|
}
|
|
5289
5620
|
return void 0;
|
|
5290
5621
|
}
|
|
@@ -5296,6 +5627,11 @@ var _ObjectQL = class _ObjectQL {
|
|
|
5296
5627
|
this.datasourceMapping = [];
|
|
5297
5628
|
// Package manifests registry (for defaultDatasource lookup)
|
|
5298
5629
|
this.manifests = /* @__PURE__ */ new Map();
|
|
5630
|
+
// Datasource definitions by name (ADR-0015): carries schemaMode +
|
|
5631
|
+
// external.allowWrites so the write gate (Gate 3) can enforce federation
|
|
5632
|
+
// ownership. Populated from manifests in registerApp and via
|
|
5633
|
+
// registerDatasourceDef. Absent entry ⇒ treated as managed (default DB).
|
|
5634
|
+
this.datasourceDefs = /* @__PURE__ */ new Map();
|
|
5299
5635
|
// Per-object hooks with priority support
|
|
5300
5636
|
this.hooks = /* @__PURE__ */ new Map([
|
|
5301
5637
|
["beforeFind", []],
|
|
@@ -5678,7 +6014,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
5678
6014
|
if (f.defaultValue == null) continue;
|
|
5679
6015
|
const dv = f.defaultValue;
|
|
5680
6016
|
if (typeof dv === "object" && dv !== null && dv.dialect && typeof dv.source === "string") {
|
|
5681
|
-
const result =
|
|
6017
|
+
const result = import_formula3.ExpressionEngine.evaluate(dv, {
|
|
5682
6018
|
now,
|
|
5683
6019
|
user: execCtx?.userId ? { id: String(execCtx.userId), role: execCtx?.roles?.[0] } : void 0,
|
|
5684
6020
|
org: execCtx?.tenantId ? { id: String(execCtx.tenantId) } : void 0,
|
|
@@ -5718,6 +6054,12 @@ var _ObjectQL = class _ObjectQL {
|
|
|
5718
6054
|
if (id) {
|
|
5719
6055
|
this.manifests.set(id, manifest);
|
|
5720
6056
|
}
|
|
6057
|
+
if (manifest.datasources) {
|
|
6058
|
+
const dsList = Array.isArray(manifest.datasources) ? manifest.datasources : Object.entries(manifest.datasources).map(([name, def]) => ({ name, ...def }));
|
|
6059
|
+
for (const ds of dsList) {
|
|
6060
|
+
if (ds?.name) this.registerDatasourceDef(ds);
|
|
6061
|
+
}
|
|
6062
|
+
}
|
|
5721
6063
|
this._registry.installPackage(manifest);
|
|
5722
6064
|
this.logger.debug("Installed Package", { id: manifest.id, name: manifest.name, namespace });
|
|
5723
6065
|
if (manifest.objects) {
|
|
@@ -5771,6 +6113,15 @@ var _ObjectQL = class _ObjectQL {
|
|
|
5771
6113
|
this._registry.registerApp(resolved, id);
|
|
5772
6114
|
this.logger.debug("Registered manifest-as-app", { app: manifest.name, from: id });
|
|
5773
6115
|
}
|
|
6116
|
+
if (Array.isArray(manifest.navigationContributions) && manifest.navigationContributions.length > 0) {
|
|
6117
|
+
for (const contribution of manifest.navigationContributions) {
|
|
6118
|
+
this._registry.registerAppNavContribution(contribution, id);
|
|
6119
|
+
}
|
|
6120
|
+
this.logger.debug("Registered navigation contributions", {
|
|
6121
|
+
from: id,
|
|
6122
|
+
count: manifest.navigationContributions.length
|
|
6123
|
+
});
|
|
6124
|
+
}
|
|
5774
6125
|
const metadataArrayKeys = [
|
|
5775
6126
|
// UI Protocol
|
|
5776
6127
|
"actions",
|
|
@@ -5793,6 +6144,8 @@ var _ObjectQL = class _ObjectQL {
|
|
|
5793
6144
|
"policies",
|
|
5794
6145
|
// AI Protocol
|
|
5795
6146
|
"agents",
|
|
6147
|
+
"tools",
|
|
6148
|
+
"skills",
|
|
5796
6149
|
"ragPipelines",
|
|
5797
6150
|
// API Protocol
|
|
5798
6151
|
"apis",
|
|
@@ -5812,6 +6165,11 @@ var _ObjectQL = class _ObjectQL {
|
|
|
5812
6165
|
if (itemName) {
|
|
5813
6166
|
const toRegister = item.name === itemName ? item : { ...item, name: itemName };
|
|
5814
6167
|
this._registry.registerItem((0, import_shared5.pluralToSingular)(key), toRegister, "name", id);
|
|
6168
|
+
if (key === "views" && (0, import_spec.isAggregatedViewContainer)(toRegister)) {
|
|
6169
|
+
for (const vi of (0, import_spec.expandViewContainer)(itemName, toRegister)) {
|
|
6170
|
+
this._registry.registerItem("view", vi, "name", id);
|
|
6171
|
+
}
|
|
6172
|
+
}
|
|
5815
6173
|
} else {
|
|
5816
6174
|
this.logger.warn(`Skipping ${(0, import_shared5.pluralToSingular)(key)} without a derivable name`, { id });
|
|
5817
6175
|
}
|
|
@@ -5964,6 +6322,41 @@ var _ObjectQL = class _ObjectQL {
|
|
|
5964
6322
|
this.logger.info("Set default driver", { driverName: driver.name });
|
|
5965
6323
|
}
|
|
5966
6324
|
}
|
|
6325
|
+
/**
|
|
6326
|
+
* Register a Datasource *definition* (ADR-0015).
|
|
6327
|
+
*
|
|
6328
|
+
* Distinct from {@link registerDriver}, which registers a live connection.
|
|
6329
|
+
* This captures the declarative `schemaMode` + `external.allowWrites` so the
|
|
6330
|
+
* write gate ({@link assertWriteAllowed}) can enforce external-datasource
|
|
6331
|
+
* ownership. Safe to call repeatedly; last write wins.
|
|
6332
|
+
*/
|
|
6333
|
+
registerDatasourceDef(def) {
|
|
6334
|
+
if (!def?.name) return;
|
|
6335
|
+
this.datasourceDefs.set(def.name, { schemaMode: def.schemaMode, external: def.external });
|
|
6336
|
+
}
|
|
6337
|
+
/**
|
|
6338
|
+
* Write gate — Gate 3 of ADR-0015 §5.3.
|
|
6339
|
+
*
|
|
6340
|
+
* Blocks insert/update/delete against a federated datasource
|
|
6341
|
+
* (`schemaMode !== 'managed'`) unless BOTH the datasource opts in
|
|
6342
|
+
* (`external.allowWrites`) AND the object opts in (`external.writable`).
|
|
6343
|
+
* Managed datasources (the common case, including the absence of any
|
|
6344
|
+
* definition) are unaffected.
|
|
6345
|
+
*/
|
|
6346
|
+
assertWriteAllowed(objectName, operation) {
|
|
6347
|
+
const object = this._registry.getObject(objectName);
|
|
6348
|
+
const dsName = object?.datasource;
|
|
6349
|
+
if (!dsName || dsName === "default") return;
|
|
6350
|
+
const ds = this.datasourceDefs.get(dsName);
|
|
6351
|
+
if (!ds || !ds.schemaMode || ds.schemaMode === "managed") return;
|
|
6352
|
+
const dsAllows = ds.external?.allowWrites ?? false;
|
|
6353
|
+
const objAllows = object?.external?.writable ?? false;
|
|
6354
|
+
if (!(dsAllows && objAllows)) {
|
|
6355
|
+
throw new import_shared5.ExternalWriteForbiddenError(
|
|
6356
|
+
`Write '${operation}' blocked on object '${objectName}': datasource '${dsName}' is external (schemaMode=${ds.schemaMode}). Requires datasource.external.allowWrites=true (got ${dsAllows}) AND object.external.writable=true (got ${objAllows}).`
|
|
6357
|
+
);
|
|
6358
|
+
}
|
|
6359
|
+
}
|
|
5967
6360
|
/**
|
|
5968
6361
|
* Set the realtime service for publishing data change events.
|
|
5969
6362
|
* Should be called after kernel resolves the realtime service.
|
|
@@ -5974,6 +6367,141 @@ var _ObjectQL = class _ObjectQL {
|
|
|
5974
6367
|
this.realtimeService = service;
|
|
5975
6368
|
this.logger.info("RealtimeService configured for data events");
|
|
5976
6369
|
}
|
|
6370
|
+
/**
|
|
6371
|
+
* Register the crypto provider that backs `secret`-typed fields.
|
|
6372
|
+
*
|
|
6373
|
+
* When set, the engine encrypts secret fields on write (storing ciphertext in
|
|
6374
|
+
* `sys_secret` and only an opaque ref on the business row) and masks them on
|
|
6375
|
+
* read. When NOT set, writing to an object that declares a secret field is
|
|
6376
|
+
* **fail-closed** — the write throws rather than persist cleartext.
|
|
6377
|
+
*
|
|
6378
|
+
* Mirrors the Settings subsystem's ICryptoProvider wiring; the host (e.g.
|
|
6379
|
+
* `serve`) injects `InMemoryCryptoProvider` in dev and a KMS/Vault-backed
|
|
6380
|
+
* provider in production.
|
|
6381
|
+
*/
|
|
6382
|
+
setCryptoProvider(provider) {
|
|
6383
|
+
this.cryptoProvider = provider;
|
|
6384
|
+
this.logger.info("CryptoProvider configured for secret fields");
|
|
6385
|
+
}
|
|
6386
|
+
/**
|
|
6387
|
+
* Encrypt any `secret`-typed fields on `row` in place before it reaches the
|
|
6388
|
+
* driver. Each plaintext is wrapped by the ICryptoProvider, persisted as a
|
|
6389
|
+
* `sys_secret` row, and replaced on `row` by an opaque ref. Cleartext never
|
|
6390
|
+
* reaches the business table.
|
|
6391
|
+
*
|
|
6392
|
+
* Rules:
|
|
6393
|
+
* - No secret fields on the object ⇒ no-op (fast path, no crypto cost).
|
|
6394
|
+
* - `null`/`undefined` value ⇒ left as-is (clears the secret).
|
|
6395
|
+
* - Value already a ref (re-save of an unchanged ref) ⇒ left as-is.
|
|
6396
|
+
* - Value equal to the read mask ⇒ dropped, so a form round-trip that
|
|
6397
|
+
* echoes the mask does not overwrite the stored secret.
|
|
6398
|
+
* - **Fail-closed:** any other value with no CryptoProvider registered, or
|
|
6399
|
+
* no reachable `sys_secret` store, THROWS — never persists cleartext.
|
|
6400
|
+
*/
|
|
6401
|
+
async encryptSecretFields(object, row, context, driverOptions) {
|
|
6402
|
+
if (!row || typeof row !== "object") return;
|
|
6403
|
+
const schema = this._registry.getObject(object);
|
|
6404
|
+
const secretFields = collectSecretFields(schema);
|
|
6405
|
+
if (secretFields.length === 0) return;
|
|
6406
|
+
for (const field of secretFields) {
|
|
6407
|
+
if (!(field in row)) continue;
|
|
6408
|
+
const value = row[field];
|
|
6409
|
+
if (value === null || typeof value === "undefined") continue;
|
|
6410
|
+
if (isSecretRef(value)) continue;
|
|
6411
|
+
if (value === SECRET_MASK) {
|
|
6412
|
+
delete row[field];
|
|
6413
|
+
continue;
|
|
6414
|
+
}
|
|
6415
|
+
if (!this.cryptoProvider) {
|
|
6416
|
+
throw new Error(
|
|
6417
|
+
`Cannot persist secret field "${object}.${field}": no CryptoProvider is registered. Wire one via engine.setCryptoProvider(...) (e.g. InMemoryCryptoProvider in dev, a KMS/Vault provider in production). Refusing to store cleartext (fail-closed).`
|
|
6418
|
+
);
|
|
6419
|
+
}
|
|
6420
|
+
const plain = typeof value === "string" ? value : JSON.stringify(value);
|
|
6421
|
+
const handle = await this.cryptoProvider.encrypt(plain, {
|
|
6422
|
+
namespace: object,
|
|
6423
|
+
key: field,
|
|
6424
|
+
tenantId: context?.tenantId
|
|
6425
|
+
});
|
|
6426
|
+
let secretDriver;
|
|
6427
|
+
try {
|
|
6428
|
+
secretDriver = this.getDriver("sys_secret");
|
|
6429
|
+
} catch {
|
|
6430
|
+
throw new Error(
|
|
6431
|
+
`Cannot persist secret field "${object}.${field}": the sys_secret store is not available. Ensure the platform-objects (sys_secret) are registered before writing secret fields (fail-closed).`
|
|
6432
|
+
);
|
|
6433
|
+
}
|
|
6434
|
+
await secretDriver.create(
|
|
6435
|
+
"sys_secret",
|
|
6436
|
+
{
|
|
6437
|
+
id: handle.id,
|
|
6438
|
+
namespace: object,
|
|
6439
|
+
key: field,
|
|
6440
|
+
kms_key_id: handle.kmsKeyId,
|
|
6441
|
+
alg: handle.alg,
|
|
6442
|
+
version: handle.version,
|
|
6443
|
+
ciphertext: handle.ciphertext,
|
|
6444
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
6445
|
+
},
|
|
6446
|
+
driverOptions
|
|
6447
|
+
);
|
|
6448
|
+
row[field] = makeSecretRef(handle.id);
|
|
6449
|
+
}
|
|
6450
|
+
}
|
|
6451
|
+
/**
|
|
6452
|
+
* Mask `secret`-typed fields on read so plaintext never leaves the engine
|
|
6453
|
+
* through the normal query path. A set secret becomes {@link SECRET_MASK};
|
|
6454
|
+
* an unset one stays `null`. Privileged callers that genuinely need the
|
|
6455
|
+
* plaintext use {@link resolveSecret} against the stored ref.
|
|
6456
|
+
*/
|
|
6457
|
+
maskSecretFields(object, rows) {
|
|
6458
|
+
if (!rows) return;
|
|
6459
|
+
const schema = this._registry.getObject(object);
|
|
6460
|
+
const secretFields = collectSecretFields(schema);
|
|
6461
|
+
if (secretFields.length === 0) return;
|
|
6462
|
+
const list = Array.isArray(rows) ? rows : [rows];
|
|
6463
|
+
for (const row of list) {
|
|
6464
|
+
if (!row || typeof row !== "object") continue;
|
|
6465
|
+
for (const field of secretFields) {
|
|
6466
|
+
if (!(field in row)) continue;
|
|
6467
|
+
row[field] = row[field] == null ? null : SECRET_MASK;
|
|
6468
|
+
}
|
|
6469
|
+
}
|
|
6470
|
+
}
|
|
6471
|
+
/**
|
|
6472
|
+
* Dereference a stored secret ref back to its plaintext. Intended for
|
|
6473
|
+
* privileged, server-side consumers (e.g. a datasource connection-pool
|
|
6474
|
+
* binder) — NOT exposed through the generic read path, which only ever
|
|
6475
|
+
* returns the mask.
|
|
6476
|
+
*
|
|
6477
|
+
* Fail-closed: throws when no CryptoProvider is registered or the
|
|
6478
|
+
* `sys_secret` row is missing. Returns `null` when `ref` is not a secret ref.
|
|
6479
|
+
*/
|
|
6480
|
+
async resolveSecret(ref, opts) {
|
|
6481
|
+
const id = parseSecretRef(ref);
|
|
6482
|
+
if (!id) return null;
|
|
6483
|
+
if (!this.cryptoProvider) {
|
|
6484
|
+
throw new Error("Cannot resolve secret: no CryptoProvider is registered (fail-closed).");
|
|
6485
|
+
}
|
|
6486
|
+
const secretDriver = this.getDriver("sys_secret");
|
|
6487
|
+
const found = await secretDriver.find("sys_secret", { object: "sys_secret", where: { id } });
|
|
6488
|
+
const secret = Array.isArray(found) ? found[0] : found;
|
|
6489
|
+
if (!secret) {
|
|
6490
|
+
throw new Error(`Cannot resolve secret: sys_secret row "${id}" not found (fail-closed).`);
|
|
6491
|
+
}
|
|
6492
|
+
const handle = {
|
|
6493
|
+
id: secret.id,
|
|
6494
|
+
kmsKeyId: secret.kms_key_id,
|
|
6495
|
+
alg: secret.alg,
|
|
6496
|
+
version: secret.version,
|
|
6497
|
+
ciphertext: secret.ciphertext
|
|
6498
|
+
};
|
|
6499
|
+
return this.cryptoProvider.decrypt(handle, {
|
|
6500
|
+
namespace: secret.namespace,
|
|
6501
|
+
key: secret.key,
|
|
6502
|
+
tenantId: opts?.tenantId
|
|
6503
|
+
});
|
|
6504
|
+
}
|
|
5977
6505
|
/**
|
|
5978
6506
|
* Helper to get object definition
|
|
5979
6507
|
*/
|
|
@@ -6266,6 +6794,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6266
6794
|
hookContext.event = "afterFind";
|
|
6267
6795
|
hookContext.result = result;
|
|
6268
6796
|
await this.triggerHooks("afterFind", hookContext);
|
|
6797
|
+
this.maskSecretFields(object, hookContext.result);
|
|
6269
6798
|
return hookContext.result;
|
|
6270
6799
|
} catch (e) {
|
|
6271
6800
|
this.logger.error("Find operation failed", e, { object });
|
|
@@ -6307,6 +6836,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6307
6836
|
const expanded = await this.expandRelatedRecords(objectName, [result], ast.expand, 0, opCtx.context);
|
|
6308
6837
|
result = expanded[0];
|
|
6309
6838
|
}
|
|
6839
|
+
this.maskSecretFields(objectName, result);
|
|
6310
6840
|
return result;
|
|
6311
6841
|
});
|
|
6312
6842
|
return opCtx.result;
|
|
@@ -6314,6 +6844,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6314
6844
|
async insert(object, data, options) {
|
|
6315
6845
|
object = this.resolveObjectName(object);
|
|
6316
6846
|
this.logger.debug("Insert operation starting", { object, isBatch: Array.isArray(data) });
|
|
6847
|
+
this.assertWriteAllowed(object, "insert");
|
|
6317
6848
|
const driver = this.getDriver(object);
|
|
6318
6849
|
const opCtx = {
|
|
6319
6850
|
object,
|
|
@@ -6342,7 +6873,13 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6342
6873
|
const rows = hookContext.input.data.map(
|
|
6343
6874
|
(row) => this.applyFieldDefaults(object, row, opCtx.context, nowSnap)
|
|
6344
6875
|
);
|
|
6345
|
-
for (const r of rows)
|
|
6876
|
+
for (const r of rows) {
|
|
6877
|
+
await this.encryptSecretFields(object, r, opCtx.context, hookContext.input.options);
|
|
6878
|
+
}
|
|
6879
|
+
for (const r of rows) {
|
|
6880
|
+
validateRecord(schemaForValidation, r, "insert");
|
|
6881
|
+
evaluateValidationRules(schemaForValidation, r, "insert", { logger: this.logger });
|
|
6882
|
+
}
|
|
6346
6883
|
if (driver.bulkCreate) {
|
|
6347
6884
|
result = await driver.bulkCreate(object, rows, hookContext.input.options);
|
|
6348
6885
|
} else {
|
|
@@ -6355,7 +6892,9 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6355
6892
|
opCtx.context,
|
|
6356
6893
|
nowSnap
|
|
6357
6894
|
);
|
|
6895
|
+
await this.encryptSecretFields(object, row, opCtx.context, hookContext.input.options);
|
|
6358
6896
|
validateRecord(schemaForValidation, row, "insert");
|
|
6897
|
+
evaluateValidationRules(schemaForValidation, row, "insert", { logger: this.logger });
|
|
6359
6898
|
result = await driver.create(object, row, hookContext.input.options);
|
|
6360
6899
|
}
|
|
6361
6900
|
hookContext.event = "afterInsert";
|
|
@@ -6405,6 +6944,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6405
6944
|
async update(object, data, options) {
|
|
6406
6945
|
object = this.resolveObjectName(object);
|
|
6407
6946
|
this.logger.debug("Update operation starting", { object });
|
|
6947
|
+
this.assertWriteAllowed(object, "update");
|
|
6408
6948
|
const driver = this.getDriver(object);
|
|
6409
6949
|
let id = data.id;
|
|
6410
6950
|
if (!id && options?.where && typeof options.where === "object" && "id" in options.where) {
|
|
@@ -6431,11 +6971,23 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6431
6971
|
hookContext.input.options = this.buildDriverOptions(opCtx.context, hookContext.input.options);
|
|
6432
6972
|
try {
|
|
6433
6973
|
let result;
|
|
6974
|
+
let priorRecord = null;
|
|
6975
|
+
const updateSchema = this._registry.getObject(object);
|
|
6434
6976
|
if (hookContext.input.id) {
|
|
6435
|
-
|
|
6977
|
+
await this.encryptSecretFields(object, hookContext.input.data, opCtx.context, hookContext.input.options);
|
|
6978
|
+
validateRecord(updateSchema, hookContext.input.data, "update");
|
|
6979
|
+
if (needsPriorRecord(updateSchema) || (this.hooks.get("afterUpdate")?.length ?? 0) > 0) {
|
|
6980
|
+
const priorAst = { object, where: { id: hookContext.input.id }, limit: 1 };
|
|
6981
|
+
priorRecord = await driver.findOne(object, priorAst, hookContext.input.options);
|
|
6982
|
+
}
|
|
6983
|
+
evaluateValidationRules(updateSchema, hookContext.input.data, "update", { previous: priorRecord, logger: this.logger });
|
|
6436
6984
|
result = await driver.update(object, hookContext.input.id, hookContext.input.data, hookContext.input.options);
|
|
6437
6985
|
} else if (options?.multi && driver.updateMany) {
|
|
6438
|
-
|
|
6986
|
+
await this.encryptSecretFields(object, hookContext.input.data, opCtx.context, hookContext.input.options);
|
|
6987
|
+
validateRecord(updateSchema, hookContext.input.data, "update");
|
|
6988
|
+
if (needsPriorRecord(updateSchema)) {
|
|
6989
|
+
this.logger.warn("Object-level validation rules (state_machine/cross_field/script) are not enforced on multi-row updates", { object });
|
|
6990
|
+
}
|
|
6439
6991
|
const ast = { object, where: options.where };
|
|
6440
6992
|
result = await driver.updateMany(object, ast, hookContext.input.data, hookContext.input.options);
|
|
6441
6993
|
} else {
|
|
@@ -6443,6 +6995,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6443
6995
|
}
|
|
6444
6996
|
hookContext.event = "afterUpdate";
|
|
6445
6997
|
hookContext.result = result;
|
|
6998
|
+
if (priorRecord) hookContext.previous = priorRecord;
|
|
6446
6999
|
await this.triggerHooks("afterUpdate", hookContext);
|
|
6447
7000
|
if (this.realtimeService) {
|
|
6448
7001
|
try {
|
|
@@ -6475,6 +7028,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6475
7028
|
async delete(object, options) {
|
|
6476
7029
|
object = this.resolveObjectName(object);
|
|
6477
7030
|
this.logger.debug("Delete operation starting", { object });
|
|
7031
|
+
this.assertWriteAllowed(object, "delete");
|
|
6478
7032
|
const driver = this.getDriver(object);
|
|
6479
7033
|
let id = void 0;
|
|
6480
7034
|
if (options?.where && typeof options.where === "object" && "id" in options.where) {
|
|
@@ -6734,6 +7288,26 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6734
7288
|
getDriverByName(name) {
|
|
6735
7289
|
return this.drivers.get(name);
|
|
6736
7290
|
}
|
|
7291
|
+
/**
|
|
7292
|
+
* Introspect a datasource's live remote schema (ADR-0015).
|
|
7293
|
+
*
|
|
7294
|
+
* Resolves the driver registered under `datasource` and delegates to its
|
|
7295
|
+
* `introspectSchema()` capability. Used by the external-datasource service
|
|
7296
|
+
* (and CLI/REST) to list remote tables and validate federated objects.
|
|
7297
|
+
*
|
|
7298
|
+
* @throws if the datasource has no registered driver, or the driver does
|
|
7299
|
+
* not support introspection.
|
|
7300
|
+
*/
|
|
7301
|
+
async introspectDatasource(datasource) {
|
|
7302
|
+
const driver = this.drivers.get(datasource);
|
|
7303
|
+
if (!driver) {
|
|
7304
|
+
throw new Error(`[ObjectQL] Datasource '${datasource}' has no registered driver to introspect.`);
|
|
7305
|
+
}
|
|
7306
|
+
if (typeof driver.introspectSchema !== "function") {
|
|
7307
|
+
throw new Error(`[ObjectQL] Driver for datasource '${datasource}' does not support introspectSchema().`);
|
|
7308
|
+
}
|
|
7309
|
+
return driver.introspectSchema();
|
|
7310
|
+
}
|
|
6737
7311
|
/**
|
|
6738
7312
|
* Get the driver responsible for the given object.
|
|
6739
7313
|
*
|
|
@@ -7667,7 +8241,7 @@ var ObjectQLPlugin = class {
|
|
|
7667
8241
|
*/
|
|
7668
8242
|
async loadMetadataFromService(metadataService, ctx) {
|
|
7669
8243
|
ctx.logger.info("Syncing metadata from external service into ObjectQL registry...");
|
|
7670
|
-
const metadataTypes = ["object", "view", "app", "flow", "
|
|
8244
|
+
const metadataTypes = ["object", "view", "app", "flow", "function", "hook"];
|
|
7671
8245
|
let totalLoaded = 0;
|
|
7672
8246
|
for (const type of metadataTypes) {
|
|
7673
8247
|
try {
|
|
@@ -7818,6 +8392,8 @@ function convertIntrospectedSchemaToObjects(introspectedSchema, options) {
|
|
|
7818
8392
|
ObjectRepository,
|
|
7819
8393
|
ObjectStackProtocolImplementation,
|
|
7820
8394
|
RESERVED_NAMESPACES,
|
|
8395
|
+
SECRET_MASK,
|
|
8396
|
+
SECRET_REF_PREFIX,
|
|
7821
8397
|
SchemaRegistry,
|
|
7822
8398
|
ScopedContext,
|
|
7823
8399
|
SysMetadataRepository,
|
|
@@ -7826,11 +8402,18 @@ function convertIntrospectedSchemaToObjects(introspectedSchema, options) {
|
|
|
7826
8402
|
applySystemFields,
|
|
7827
8403
|
bindHooksToEngine,
|
|
7828
8404
|
bucketDateValue,
|
|
8405
|
+
collectSecretFields,
|
|
7829
8406
|
computeFQN,
|
|
7830
8407
|
convertIntrospectedSchemaToObjects,
|
|
7831
8408
|
createObjectQLKernel,
|
|
8409
|
+
evaluateValidationRules,
|
|
8410
|
+
isSecretRef,
|
|
8411
|
+
legalNextStates,
|
|
8412
|
+
makeSecretRef,
|
|
8413
|
+
needsPriorRecord,
|
|
7832
8414
|
noopHookMetricsRecorder,
|
|
7833
8415
|
parseFQN,
|
|
8416
|
+
parseSecretRef,
|
|
7834
8417
|
toTitleCase,
|
|
7835
8418
|
validateRecord,
|
|
7836
8419
|
wrapDeclarativeHook
|