@objectstack/objectql 7.3.0 → 7.4.1
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 +322 -9
- package/dist/index.d.ts +322 -9
- package/dist/index.js +538 -14
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +531 -16
- 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,14 @@ 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();
|
|
181
198
|
/**
|
|
182
199
|
* Package ids that must be installed in a DISABLED state. Seeded once at
|
|
183
200
|
* boot (from persisted state) BEFORE any package registration so that every
|
|
@@ -405,6 +422,47 @@ var SchemaRegistry = class {
|
|
|
405
422
|
const contributors = this.objectContributors.get(fqn);
|
|
406
423
|
return contributors?.find((c) => c.ownership === "own");
|
|
407
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
|
+
}
|
|
408
466
|
/**
|
|
409
467
|
* Unregister all objects contributed by a package.
|
|
410
468
|
*
|
|
@@ -646,10 +704,88 @@ var SchemaRegistry = class {
|
|
|
646
704
|
this.registerItem("app", app, "name", packageId);
|
|
647
705
|
}
|
|
648
706
|
getApp(name) {
|
|
649
|
-
|
|
707
|
+
const app = this.getItem("app", name);
|
|
708
|
+
if (!app) return app;
|
|
709
|
+
return this.applyNavContributions(app);
|
|
650
710
|
}
|
|
651
711
|
getAllApps() {
|
|
652
|
-
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;
|
|
653
789
|
}
|
|
654
790
|
// ==========================================
|
|
655
791
|
// Plugin Helpers
|
|
@@ -708,6 +844,7 @@ var SchemaRegistry = class {
|
|
|
708
844
|
this.mergedObjectCache.clear();
|
|
709
845
|
this.namespaceRegistry.clear();
|
|
710
846
|
this.metadata.clear();
|
|
847
|
+
this.appNavContributions.clear();
|
|
711
848
|
this.log("[Registry] Reset complete");
|
|
712
849
|
}
|
|
713
850
|
};
|
|
@@ -1399,6 +1536,7 @@ var SysMetadataRepository = class {
|
|
|
1399
1536
|
var import_metadata_core2 = require("@objectstack/metadata-core");
|
|
1400
1537
|
var import_data2 = require("@objectstack/spec/data");
|
|
1401
1538
|
var import_shared4 = require("@objectstack/spec/shared");
|
|
1539
|
+
var import_ui2 = require("@objectstack/spec/ui");
|
|
1402
1540
|
var import_system = require("@objectstack/spec/system");
|
|
1403
1541
|
var import_kernel4 = require("@objectstack/spec/kernel");
|
|
1404
1542
|
var import_kernel5 = require("@objectstack/spec/kernel");
|
|
@@ -2093,6 +2231,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2093
2231
|
const zodSchema = (0, import_kernel4.getMetadataTypeSchema)(singular);
|
|
2094
2232
|
const schema = (zodSchema ? toJsonSchemaSafe(zodSchema) : void 0) ?? HAND_CRAFTED_SCHEMAS[singular];
|
|
2095
2233
|
const form = TYPE_TO_FORM[singular];
|
|
2234
|
+
const typeActions = (0, import_kernel4.getMetadataTypeActions)(singular);
|
|
2096
2235
|
const base = registryByType.get(singular);
|
|
2097
2236
|
if (base) {
|
|
2098
2237
|
const isEnvOverridden = writableOverrides.has(singular);
|
|
@@ -2104,7 +2243,11 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2104
2243
|
allowOrgOverride: base.allowOrgOverride || isEnvOverridden,
|
|
2105
2244
|
overrideSource: isEnvOverridden && !base.allowOrgOverride ? "env" : "registry",
|
|
2106
2245
|
schema,
|
|
2107
|
-
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 } : {}
|
|
2108
2251
|
};
|
|
2109
2252
|
}
|
|
2110
2253
|
return {
|
|
@@ -2123,7 +2266,9 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2123
2266
|
domain: "system",
|
|
2124
2267
|
overrideSource: writableOverrides.has(singular) ? "env" : "registry",
|
|
2125
2268
|
schema,
|
|
2126
|
-
form
|
|
2269
|
+
form,
|
|
2270
|
+
// Plugin-registered actions on a type with no registry entry.
|
|
2271
|
+
...typeActions.length ? { actions: typeActions } : {}
|
|
2127
2272
|
};
|
|
2128
2273
|
}).sort((a, b) => {
|
|
2129
2274
|
if (a.domain !== b.domain) return a.domain.localeCompare(b.domain);
|
|
@@ -2298,6 +2443,12 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2298
2443
|
(it) => !this.engine.registry.isPackageDisabled(it?._packageId)
|
|
2299
2444
|
);
|
|
2300
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
|
+
}
|
|
2301
2452
|
return {
|
|
2302
2453
|
type: request.type,
|
|
2303
2454
|
items: decorateMetadataItems(
|
|
@@ -2387,6 +2538,9 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2387
2538
|
if (alt) item = this.engine.registry.getItem(alt, request.name);
|
|
2388
2539
|
}
|
|
2389
2540
|
}
|
|
2541
|
+
if ((request.type === "app" || request.type === "apps") && item) {
|
|
2542
|
+
item = this.engine.registry.applyNavContributions(item);
|
|
2543
|
+
}
|
|
2390
2544
|
const artifactItem = this.lookupArtifactItem(request.type, request.name);
|
|
2391
2545
|
const decorated = decorateMetadataItem(
|
|
2392
2546
|
request.type,
|
|
@@ -3145,7 +3299,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3145
3299
|
throw new Error(`Metadata item ${request.type}/${request.name} not found`);
|
|
3146
3300
|
}
|
|
3147
3301
|
const content = JSON.stringify(item);
|
|
3148
|
-
const hash = simpleHash(content);
|
|
3302
|
+
const hash = simpleHash(request.locale ? `${request.locale}\0${content}` : content);
|
|
3149
3303
|
const etag = { value: hash, weak: false };
|
|
3150
3304
|
if (request.cacheRequest?.ifNoneMatch) {
|
|
3151
3305
|
const clientEtag = request.cacheRequest.ifNoneMatch.replace(/^"(.*)"$/, "$1").replace(/^W\/"(.*)"$/, "$1");
|
|
@@ -4597,8 +4751,33 @@ var ObjectStackProtocolImplementation = _ObjectStackProtocolImplementation;
|
|
|
4597
4751
|
var import_kernel6 = require("@objectstack/spec/kernel");
|
|
4598
4752
|
var import_core = require("@objectstack/core");
|
|
4599
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
|
|
4600
4778
|
var import_shared5 = require("@objectstack/spec/shared");
|
|
4601
|
-
var
|
|
4779
|
+
var import_formula3 = require("@objectstack/formula");
|
|
4780
|
+
var import_spec = require("@objectstack/spec");
|
|
4602
4781
|
|
|
4603
4782
|
// src/hook-wrappers.ts
|
|
4604
4783
|
var import_formula = require("@objectstack/formula");
|
|
@@ -5152,6 +5331,101 @@ function validateRecord(objectSchema, data, mode) {
|
|
|
5152
5331
|
if (errors.length > 0) throw new ValidationError(errors);
|
|
5153
5332
|
}
|
|
5154
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
|
+
|
|
5155
5429
|
// src/in-memory-aggregation.ts
|
|
5156
5430
|
function applyInMemoryAggregation(rows, ast) {
|
|
5157
5431
|
const groupBy = ast.groupBy ?? [];
|
|
@@ -5309,7 +5583,7 @@ function planFormulaProjection(schema, requestedFields) {
|
|
|
5309
5583
|
if (def?.type === "formula" && def.expression) {
|
|
5310
5584
|
const expr = typeof def.expression === "string" ? { dialect: "cel", source: def.expression } : def.expression;
|
|
5311
5585
|
plan.push({ name: f, expression: expr });
|
|
5312
|
-
|
|
5586
|
+
import_formula3.ExpressionEngine.compile(expr);
|
|
5313
5587
|
} else if (Array.isArray(requestedFields) && requestedFields.length > 0) {
|
|
5314
5588
|
projected.add(f);
|
|
5315
5589
|
}
|
|
@@ -5331,7 +5605,7 @@ function applyFormulaPlan(plan, records) {
|
|
|
5331
5605
|
for (const rec of records) {
|
|
5332
5606
|
if (rec == null) continue;
|
|
5333
5607
|
for (const fp of plan) {
|
|
5334
|
-
const r =
|
|
5608
|
+
const r = import_formula3.ExpressionEngine.evaluate(fp.expression, { record: rec });
|
|
5335
5609
|
rec[fp.name] = r.ok ? r.value : null;
|
|
5336
5610
|
}
|
|
5337
5611
|
}
|
|
@@ -5341,7 +5615,7 @@ function resolveMetadataItemName(key, item) {
|
|
|
5341
5615
|
if (item.name) return item.name;
|
|
5342
5616
|
if (item.id) return item.id;
|
|
5343
5617
|
if (key === "views") {
|
|
5344
|
-
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;
|
|
5345
5619
|
}
|
|
5346
5620
|
return void 0;
|
|
5347
5621
|
}
|
|
@@ -5353,6 +5627,11 @@ var _ObjectQL = class _ObjectQL {
|
|
|
5353
5627
|
this.datasourceMapping = [];
|
|
5354
5628
|
// Package manifests registry (for defaultDatasource lookup)
|
|
5355
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();
|
|
5356
5635
|
// Per-object hooks with priority support
|
|
5357
5636
|
this.hooks = /* @__PURE__ */ new Map([
|
|
5358
5637
|
["beforeFind", []],
|
|
@@ -5735,7 +6014,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
5735
6014
|
if (f.defaultValue == null) continue;
|
|
5736
6015
|
const dv = f.defaultValue;
|
|
5737
6016
|
if (typeof dv === "object" && dv !== null && dv.dialect && typeof dv.source === "string") {
|
|
5738
|
-
const result =
|
|
6017
|
+
const result = import_formula3.ExpressionEngine.evaluate(dv, {
|
|
5739
6018
|
now,
|
|
5740
6019
|
user: execCtx?.userId ? { id: String(execCtx.userId), role: execCtx?.roles?.[0] } : void 0,
|
|
5741
6020
|
org: execCtx?.tenantId ? { id: String(execCtx.tenantId) } : void 0,
|
|
@@ -5775,6 +6054,12 @@ var _ObjectQL = class _ObjectQL {
|
|
|
5775
6054
|
if (id) {
|
|
5776
6055
|
this.manifests.set(id, manifest);
|
|
5777
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
|
+
}
|
|
5778
6063
|
this._registry.installPackage(manifest);
|
|
5779
6064
|
this.logger.debug("Installed Package", { id: manifest.id, name: manifest.name, namespace });
|
|
5780
6065
|
if (manifest.objects) {
|
|
@@ -5828,6 +6113,15 @@ var _ObjectQL = class _ObjectQL {
|
|
|
5828
6113
|
this._registry.registerApp(resolved, id);
|
|
5829
6114
|
this.logger.debug("Registered manifest-as-app", { app: manifest.name, from: id });
|
|
5830
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
|
+
}
|
|
5831
6125
|
const metadataArrayKeys = [
|
|
5832
6126
|
// UI Protocol
|
|
5833
6127
|
"actions",
|
|
@@ -5871,6 +6165,11 @@ var _ObjectQL = class _ObjectQL {
|
|
|
5871
6165
|
if (itemName) {
|
|
5872
6166
|
const toRegister = item.name === itemName ? item : { ...item, name: itemName };
|
|
5873
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
|
+
}
|
|
5874
6173
|
} else {
|
|
5875
6174
|
this.logger.warn(`Skipping ${(0, import_shared5.pluralToSingular)(key)} without a derivable name`, { id });
|
|
5876
6175
|
}
|
|
@@ -6023,6 +6322,41 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6023
6322
|
this.logger.info("Set default driver", { driverName: driver.name });
|
|
6024
6323
|
}
|
|
6025
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
|
+
}
|
|
6026
6360
|
/**
|
|
6027
6361
|
* Set the realtime service for publishing data change events.
|
|
6028
6362
|
* Should be called after kernel resolves the realtime service.
|
|
@@ -6033,6 +6367,141 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6033
6367
|
this.realtimeService = service;
|
|
6034
6368
|
this.logger.info("RealtimeService configured for data events");
|
|
6035
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
|
+
}
|
|
6036
6505
|
/**
|
|
6037
6506
|
* Helper to get object definition
|
|
6038
6507
|
*/
|
|
@@ -6325,6 +6794,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6325
6794
|
hookContext.event = "afterFind";
|
|
6326
6795
|
hookContext.result = result;
|
|
6327
6796
|
await this.triggerHooks("afterFind", hookContext);
|
|
6797
|
+
this.maskSecretFields(object, hookContext.result);
|
|
6328
6798
|
return hookContext.result;
|
|
6329
6799
|
} catch (e) {
|
|
6330
6800
|
this.logger.error("Find operation failed", e, { object });
|
|
@@ -6366,6 +6836,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6366
6836
|
const expanded = await this.expandRelatedRecords(objectName, [result], ast.expand, 0, opCtx.context);
|
|
6367
6837
|
result = expanded[0];
|
|
6368
6838
|
}
|
|
6839
|
+
this.maskSecretFields(objectName, result);
|
|
6369
6840
|
return result;
|
|
6370
6841
|
});
|
|
6371
6842
|
return opCtx.result;
|
|
@@ -6373,6 +6844,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6373
6844
|
async insert(object, data, options) {
|
|
6374
6845
|
object = this.resolveObjectName(object);
|
|
6375
6846
|
this.logger.debug("Insert operation starting", { object, isBatch: Array.isArray(data) });
|
|
6847
|
+
this.assertWriteAllowed(object, "insert");
|
|
6376
6848
|
const driver = this.getDriver(object);
|
|
6377
6849
|
const opCtx = {
|
|
6378
6850
|
object,
|
|
@@ -6401,7 +6873,13 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6401
6873
|
const rows = hookContext.input.data.map(
|
|
6402
6874
|
(row) => this.applyFieldDefaults(object, row, opCtx.context, nowSnap)
|
|
6403
6875
|
);
|
|
6404
|
-
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
|
+
}
|
|
6405
6883
|
if (driver.bulkCreate) {
|
|
6406
6884
|
result = await driver.bulkCreate(object, rows, hookContext.input.options);
|
|
6407
6885
|
} else {
|
|
@@ -6414,7 +6892,9 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6414
6892
|
opCtx.context,
|
|
6415
6893
|
nowSnap
|
|
6416
6894
|
);
|
|
6895
|
+
await this.encryptSecretFields(object, row, opCtx.context, hookContext.input.options);
|
|
6417
6896
|
validateRecord(schemaForValidation, row, "insert");
|
|
6897
|
+
evaluateValidationRules(schemaForValidation, row, "insert", { logger: this.logger });
|
|
6418
6898
|
result = await driver.create(object, row, hookContext.input.options);
|
|
6419
6899
|
}
|
|
6420
6900
|
hookContext.event = "afterInsert";
|
|
@@ -6464,6 +6944,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6464
6944
|
async update(object, data, options) {
|
|
6465
6945
|
object = this.resolveObjectName(object);
|
|
6466
6946
|
this.logger.debug("Update operation starting", { object });
|
|
6947
|
+
this.assertWriteAllowed(object, "update");
|
|
6467
6948
|
const driver = this.getDriver(object);
|
|
6468
6949
|
let id = data.id;
|
|
6469
6950
|
if (!id && options?.where && typeof options.where === "object" && "id" in options.where) {
|
|
@@ -6490,11 +6971,23 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6490
6971
|
hookContext.input.options = this.buildDriverOptions(opCtx.context, hookContext.input.options);
|
|
6491
6972
|
try {
|
|
6492
6973
|
let result;
|
|
6974
|
+
let priorRecord = null;
|
|
6975
|
+
const updateSchema = this._registry.getObject(object);
|
|
6493
6976
|
if (hookContext.input.id) {
|
|
6494
|
-
|
|
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 });
|
|
6495
6984
|
result = await driver.update(object, hookContext.input.id, hookContext.input.data, hookContext.input.options);
|
|
6496
6985
|
} else if (options?.multi && driver.updateMany) {
|
|
6497
|
-
|
|
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
|
+
}
|
|
6498
6991
|
const ast = { object, where: options.where };
|
|
6499
6992
|
result = await driver.updateMany(object, ast, hookContext.input.data, hookContext.input.options);
|
|
6500
6993
|
} else {
|
|
@@ -6502,6 +6995,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6502
6995
|
}
|
|
6503
6996
|
hookContext.event = "afterUpdate";
|
|
6504
6997
|
hookContext.result = result;
|
|
6998
|
+
if (priorRecord) hookContext.previous = priorRecord;
|
|
6505
6999
|
await this.triggerHooks("afterUpdate", hookContext);
|
|
6506
7000
|
if (this.realtimeService) {
|
|
6507
7001
|
try {
|
|
@@ -6534,6 +7028,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6534
7028
|
async delete(object, options) {
|
|
6535
7029
|
object = this.resolveObjectName(object);
|
|
6536
7030
|
this.logger.debug("Delete operation starting", { object });
|
|
7031
|
+
this.assertWriteAllowed(object, "delete");
|
|
6537
7032
|
const driver = this.getDriver(object);
|
|
6538
7033
|
let id = void 0;
|
|
6539
7034
|
if (options?.where && typeof options.where === "object" && "id" in options.where) {
|
|
@@ -6793,6 +7288,26 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6793
7288
|
getDriverByName(name) {
|
|
6794
7289
|
return this.drivers.get(name);
|
|
6795
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
|
+
}
|
|
6796
7311
|
/**
|
|
6797
7312
|
* Get the driver responsible for the given object.
|
|
6798
7313
|
*
|
|
@@ -7726,7 +8241,7 @@ var ObjectQLPlugin = class {
|
|
|
7726
8241
|
*/
|
|
7727
8242
|
async loadMetadataFromService(metadataService, ctx) {
|
|
7728
8243
|
ctx.logger.info("Syncing metadata from external service into ObjectQL registry...");
|
|
7729
|
-
const metadataTypes = ["object", "view", "app", "flow", "
|
|
8244
|
+
const metadataTypes = ["object", "view", "app", "flow", "function", "hook"];
|
|
7730
8245
|
let totalLoaded = 0;
|
|
7731
8246
|
for (const type of metadataTypes) {
|
|
7732
8247
|
try {
|
|
@@ -7877,6 +8392,8 @@ function convertIntrospectedSchemaToObjects(introspectedSchema, options) {
|
|
|
7877
8392
|
ObjectRepository,
|
|
7878
8393
|
ObjectStackProtocolImplementation,
|
|
7879
8394
|
RESERVED_NAMESPACES,
|
|
8395
|
+
SECRET_MASK,
|
|
8396
|
+
SECRET_REF_PREFIX,
|
|
7880
8397
|
SchemaRegistry,
|
|
7881
8398
|
ScopedContext,
|
|
7882
8399
|
SysMetadataRepository,
|
|
@@ -7885,11 +8402,18 @@ function convertIntrospectedSchemaToObjects(introspectedSchema, options) {
|
|
|
7885
8402
|
applySystemFields,
|
|
7886
8403
|
bindHooksToEngine,
|
|
7887
8404
|
bucketDateValue,
|
|
8405
|
+
collectSecretFields,
|
|
7888
8406
|
computeFQN,
|
|
7889
8407
|
convertIntrospectedSchemaToObjects,
|
|
7890
8408
|
createObjectQLKernel,
|
|
8409
|
+
evaluateValidationRules,
|
|
8410
|
+
isSecretRef,
|
|
8411
|
+
legalNextStates,
|
|
8412
|
+
makeSecretRef,
|
|
8413
|
+
needsPriorRecord,
|
|
7891
8414
|
noopHookMetricsRecorder,
|
|
7892
8415
|
parseFQN,
|
|
8416
|
+
parseSecretRef,
|
|
7893
8417
|
toTitleCase,
|
|
7894
8418
|
validateRecord,
|
|
7895
8419
|
wrapDeclarativeHook
|