@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.mjs
CHANGED
|
@@ -128,6 +128,14 @@ var SchemaRegistry = class {
|
|
|
128
128
|
// ==========================================
|
|
129
129
|
/** Type → Name/ID → MetadataItem */
|
|
130
130
|
this.metadata = /* @__PURE__ */ new Map();
|
|
131
|
+
/**
|
|
132
|
+
* App name → navigation contributions (ADR-0029 D7).
|
|
133
|
+
*
|
|
134
|
+
* Lets packages inject nav items into apps they do not own (the UI analog
|
|
135
|
+
* of object extenders). Merged into the owning app's `navigation` tree on
|
|
136
|
+
* read in {@link getApp} / {@link getAllApps} by group id + priority.
|
|
137
|
+
*/
|
|
138
|
+
this.appNavContributions = /* @__PURE__ */ new Map();
|
|
131
139
|
/**
|
|
132
140
|
* Package ids that must be installed in a DISABLED state. Seeded once at
|
|
133
141
|
* boot (from persisted state) BEFORE any package registration so that every
|
|
@@ -355,6 +363,47 @@ var SchemaRegistry = class {
|
|
|
355
363
|
const contributors = this.objectContributors.get(fqn);
|
|
356
364
|
return contributors?.find((c) => c.ownership === "own");
|
|
357
365
|
}
|
|
366
|
+
/**
|
|
367
|
+
* ADR-0029 K0 — assert every registered object resolves to exactly one
|
|
368
|
+
* owner.
|
|
369
|
+
*
|
|
370
|
+
* A second `own` from a different package is already rejected eagerly in
|
|
371
|
+
* {@link registerObject} (it throws). This is the install-time backstop
|
|
372
|
+
* called once all packages are registered (kernel bootstrap complete),
|
|
373
|
+
* and it additionally catches the case `registerObject` cannot: an object
|
|
374
|
+
* that has only `extend` contributions and **no owner** — which would
|
|
375
|
+
* otherwise resolve to nothing. Surfacing it here turns a silent
|
|
376
|
+
* "extend a non-existent object" into a clear bootstrap error.
|
|
377
|
+
*
|
|
378
|
+
* This is the invariant the kernel-decomposition (ADR-0029) relies on:
|
|
379
|
+
* the `sys` namespace is shared across many first-party plugins, but each
|
|
380
|
+
* object name has exactly one owner.
|
|
381
|
+
*
|
|
382
|
+
* @throws Error listing every object whose owner count is not exactly 1.
|
|
383
|
+
*/
|
|
384
|
+
assertSingleOwnerPerObject() {
|
|
385
|
+
const violations = [];
|
|
386
|
+
for (const [fqn, contributors] of this.objectContributors.entries()) {
|
|
387
|
+
const owners = contributors.filter((c) => c.ownership === "own");
|
|
388
|
+
if (owners.length === 0) {
|
|
389
|
+
const extenders = contributors.map((c) => c.packageId).join(", ") || "(none)";
|
|
390
|
+
violations.push(
|
|
391
|
+
`Object "${fqn}" has no owner \u2014 only extend contributions from [${extenders}]. Exactly one package must register it with ownership 'own'.`
|
|
392
|
+
);
|
|
393
|
+
} else if (owners.length > 1) {
|
|
394
|
+
const names = owners.map((c) => c.packageId).join(", ");
|
|
395
|
+
violations.push(
|
|
396
|
+
`Object "${fqn}" has ${owners.length} owners [${names}] \u2014 exactly one is allowed.`
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (violations.length > 0) {
|
|
401
|
+
throw new Error(
|
|
402
|
+
`[Registry] single-owner-per-object check failed (ADR-0029):
|
|
403
|
+
` + violations.join("\n ")
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
358
407
|
/**
|
|
359
408
|
* Unregister all objects contributed by a package.
|
|
360
409
|
*
|
|
@@ -596,10 +645,88 @@ var SchemaRegistry = class {
|
|
|
596
645
|
this.registerItem("app", app, "name", packageId);
|
|
597
646
|
}
|
|
598
647
|
getApp(name) {
|
|
599
|
-
|
|
648
|
+
const app = this.getItem("app", name);
|
|
649
|
+
if (!app) return app;
|
|
650
|
+
return this.applyNavContributions(app);
|
|
600
651
|
}
|
|
601
652
|
getAllApps() {
|
|
602
|
-
return this.listItems("app");
|
|
653
|
+
return this.listItems("app").map((app) => this.applyNavContributions(app));
|
|
654
|
+
}
|
|
655
|
+
// ==========================================
|
|
656
|
+
// App navigation contributions (ADR-0029 D7)
|
|
657
|
+
// ==========================================
|
|
658
|
+
/**
|
|
659
|
+
* Register a navigation contribution — a package injecting nav items into
|
|
660
|
+
* an app it does not own (the UI-layer analog of object `extend`).
|
|
661
|
+
*
|
|
662
|
+
* Contributions are merged into the target app's `navigation` tree lazily
|
|
663
|
+
* on read ({@link getApp} / {@link getAllApps}) by group id + priority, so
|
|
664
|
+
* registration order does not matter and the owning app can be registered
|
|
665
|
+
* before or after its contributors.
|
|
666
|
+
*/
|
|
667
|
+
registerAppNavContribution(contribution, packageId) {
|
|
668
|
+
if (!contribution || !contribution.app) return;
|
|
669
|
+
const list = this.appNavContributions.get(contribution.app) ?? [];
|
|
670
|
+
list.push({
|
|
671
|
+
packageId,
|
|
672
|
+
group: contribution.group,
|
|
673
|
+
priority: contribution.priority ?? 200,
|
|
674
|
+
items: Array.isArray(contribution.items) ? contribution.items : []
|
|
675
|
+
});
|
|
676
|
+
this.appNavContributions.set(contribution.app, list);
|
|
677
|
+
this.log(
|
|
678
|
+
`[Registry] Navigation contribution: ${packageId ?? "(unknown)"} -> ${contribution.app}` + (contribution.group ? `/${contribution.group}` : "") + ` (${list[list.length - 1].items.length} items)`
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
/** Contributions registered for an app (empty array when none). */
|
|
682
|
+
getAppNavContributions(appName) {
|
|
683
|
+
return this.appNavContributions.get(appName) ?? [];
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Return a copy of `app` with all registered navigation contributions
|
|
687
|
+
* merged into its `navigation` tree. The stored app is never mutated, so
|
|
688
|
+
* repeated reads stay idempotent.
|
|
689
|
+
*
|
|
690
|
+
* Public so the protocol serving path (`getMetaItems` / `getMetaItem` for
|
|
691
|
+
* `app`) can merge contributions the same way `getApp` / `getAllApps` do —
|
|
692
|
+
* the REST app endpoints read through the protocol, not these helpers, so
|
|
693
|
+
* the merge must be reachable from there too (ADR-0029 D7).
|
|
694
|
+
*/
|
|
695
|
+
applyNavContributions(app) {
|
|
696
|
+
const contributions = this.appNavContributions.get(app?.name);
|
|
697
|
+
if (!contributions || contributions.length === 0) return app;
|
|
698
|
+
const cloned = structuredClone(app);
|
|
699
|
+
const nav = Array.isArray(cloned.navigation) ? cloned.navigation : cloned.navigation = [];
|
|
700
|
+
const sorted = [...contributions].sort((a, b) => a.priority - b.priority);
|
|
701
|
+
for (const c of sorted) {
|
|
702
|
+
if (!c.items.length) continue;
|
|
703
|
+
if (c.group) {
|
|
704
|
+
const group = this.findNavGroup(nav, c.group);
|
|
705
|
+
if (group) {
|
|
706
|
+
if (!Array.isArray(group.children)) group.children = [];
|
|
707
|
+
group.children.push(...c.items);
|
|
708
|
+
} else {
|
|
709
|
+
this.log(
|
|
710
|
+
`[Registry] Navigation contribution from "${c.packageId ?? "(unknown)"}" targets missing group "${c.group}" in app "${app.name}" \u2014 appending at top level.`
|
|
711
|
+
);
|
|
712
|
+
nav.push(...c.items);
|
|
713
|
+
}
|
|
714
|
+
} else {
|
|
715
|
+
nav.push(...c.items);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return cloned;
|
|
719
|
+
}
|
|
720
|
+
/** Depth-first search for a `type: 'group'` nav item by id. */
|
|
721
|
+
findNavGroup(items, groupId) {
|
|
722
|
+
for (const item of items) {
|
|
723
|
+
if (item && item.id === groupId && item.type === "group") return item;
|
|
724
|
+
if (item && Array.isArray(item.children)) {
|
|
725
|
+
const found = this.findNavGroup(item.children, groupId);
|
|
726
|
+
if (found) return found;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
return void 0;
|
|
603
730
|
}
|
|
604
731
|
// ==========================================
|
|
605
732
|
// Plugin Helpers
|
|
@@ -658,6 +785,7 @@ var SchemaRegistry = class {
|
|
|
658
785
|
this.mergedObjectCache.clear();
|
|
659
786
|
this.namespaceRegistry.clear();
|
|
660
787
|
this.metadata.clear();
|
|
788
|
+
this.appNavContributions.clear();
|
|
661
789
|
this.log("[Registry] Reset complete");
|
|
662
790
|
}
|
|
663
791
|
};
|
|
@@ -1349,8 +1477,9 @@ var SysMetadataRepository = class {
|
|
|
1349
1477
|
import { ConflictError as ConflictError2 } from "@objectstack/metadata-core";
|
|
1350
1478
|
import { parseFilterAST, isFilterAST } from "@objectstack/spec/data";
|
|
1351
1479
|
import { PLURAL_TO_SINGULAR as PLURAL_TO_SINGULAR3, SINGULAR_TO_PLURAL as SINGULAR_TO_PLURAL2 } from "@objectstack/spec/shared";
|
|
1480
|
+
import { isAggregatedViewContainer } from "@objectstack/spec/ui";
|
|
1352
1481
|
import { METADATA_FORM_REGISTRY } from "@objectstack/spec/system";
|
|
1353
|
-
import { DEFAULT_METADATA_TYPE_REGISTRY as DEFAULT_METADATA_TYPE_REGISTRY2, getMetadataTypeSchema as getMetadataTypeSchema2 } from "@objectstack/spec/kernel";
|
|
1482
|
+
import { DEFAULT_METADATA_TYPE_REGISTRY as DEFAULT_METADATA_TYPE_REGISTRY2, getMetadataTypeSchema as getMetadataTypeSchema2, getMetadataTypeActions } from "@objectstack/spec/kernel";
|
|
1354
1483
|
import {
|
|
1355
1484
|
extractProtection,
|
|
1356
1485
|
evaluateLockForWrite,
|
|
@@ -2048,6 +2177,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2048
2177
|
const zodSchema = getMetadataTypeSchema2(singular);
|
|
2049
2178
|
const schema = (zodSchema ? toJsonSchemaSafe(zodSchema) : void 0) ?? HAND_CRAFTED_SCHEMAS[singular];
|
|
2050
2179
|
const form = TYPE_TO_FORM[singular];
|
|
2180
|
+
const typeActions = getMetadataTypeActions(singular);
|
|
2051
2181
|
const base = registryByType.get(singular);
|
|
2052
2182
|
if (base) {
|
|
2053
2183
|
const isEnvOverridden = writableOverrides.has(singular);
|
|
@@ -2059,7 +2189,11 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2059
2189
|
allowOrgOverride: base.allowOrgOverride || isEnvOverridden,
|
|
2060
2190
|
overrideSource: isEnvOverridden && !base.allowOrgOverride ? "env" : "registry",
|
|
2061
2191
|
schema,
|
|
2062
|
-
form
|
|
2192
|
+
form,
|
|
2193
|
+
// Override the spread `base.actions` with the merged view
|
|
2194
|
+
// (declarative + plugin-registered). Omit when empty to
|
|
2195
|
+
// preserve the prior "no actions key" response shape.
|
|
2196
|
+
...typeActions.length ? { actions: typeActions } : {}
|
|
2063
2197
|
};
|
|
2064
2198
|
}
|
|
2065
2199
|
return {
|
|
@@ -2078,7 +2212,9 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2078
2212
|
domain: "system",
|
|
2079
2213
|
overrideSource: writableOverrides.has(singular) ? "env" : "registry",
|
|
2080
2214
|
schema,
|
|
2081
|
-
form
|
|
2215
|
+
form,
|
|
2216
|
+
// Plugin-registered actions on a type with no registry entry.
|
|
2217
|
+
...typeActions.length ? { actions: typeActions } : {}
|
|
2082
2218
|
};
|
|
2083
2219
|
}).sort((a, b) => {
|
|
2084
2220
|
if (a.domain !== b.domain) return a.domain.localeCompare(b.domain);
|
|
@@ -2253,6 +2389,12 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2253
2389
|
(it) => !this.engine.registry.isPackageDisabled(it?._packageId)
|
|
2254
2390
|
);
|
|
2255
2391
|
}
|
|
2392
|
+
if (request.type === "view" || request.type === "views") {
|
|
2393
|
+
items = items.filter((it) => !isAggregatedViewContainer(it));
|
|
2394
|
+
}
|
|
2395
|
+
if (request.type === "app" || request.type === "apps") {
|
|
2396
|
+
items = items.map((app) => this.engine.registry.applyNavContributions(app));
|
|
2397
|
+
}
|
|
2256
2398
|
return {
|
|
2257
2399
|
type: request.type,
|
|
2258
2400
|
items: decorateMetadataItems(
|
|
@@ -2342,6 +2484,9 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2342
2484
|
if (alt) item = this.engine.registry.getItem(alt, request.name);
|
|
2343
2485
|
}
|
|
2344
2486
|
}
|
|
2487
|
+
if ((request.type === "app" || request.type === "apps") && item) {
|
|
2488
|
+
item = this.engine.registry.applyNavContributions(item);
|
|
2489
|
+
}
|
|
2345
2490
|
const artifactItem = this.lookupArtifactItem(request.type, request.name);
|
|
2346
2491
|
const decorated = decorateMetadataItem(
|
|
2347
2492
|
request.type,
|
|
@@ -3100,7 +3245,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3100
3245
|
throw new Error(`Metadata item ${request.type}/${request.name} not found`);
|
|
3101
3246
|
}
|
|
3102
3247
|
const content = JSON.stringify(item);
|
|
3103
|
-
const hash = simpleHash(content);
|
|
3248
|
+
const hash = simpleHash(request.locale ? `${request.locale}\0${content}` : content);
|
|
3104
3249
|
const etag = { value: hash, weak: false };
|
|
3105
3250
|
if (request.cacheRequest?.ifNoneMatch) {
|
|
3106
3251
|
const clientEtag = request.cacheRequest.ifNoneMatch.replace(/^"(.*)"$/, "$1").replace(/^W\/"(.*)"$/, "$1");
|
|
@@ -4552,8 +4697,33 @@ var ObjectStackProtocolImplementation = _ObjectStackProtocolImplementation;
|
|
|
4552
4697
|
import { ExecutionContextSchema } from "@objectstack/spec/kernel";
|
|
4553
4698
|
import { createLogger } from "@objectstack/core";
|
|
4554
4699
|
import { CoreServiceName, StorageNameMapping } from "@objectstack/spec/system";
|
|
4555
|
-
|
|
4556
|
-
|
|
4700
|
+
|
|
4701
|
+
// src/secret-fields.ts
|
|
4702
|
+
var SECRET_REF_PREFIX = "secret:";
|
|
4703
|
+
var SECRET_MASK = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
|
|
4704
|
+
function makeSecretRef(handleId) {
|
|
4705
|
+
return `${SECRET_REF_PREFIX}${handleId}`;
|
|
4706
|
+
}
|
|
4707
|
+
function isSecretRef(value) {
|
|
4708
|
+
return typeof value === "string" && value.startsWith(SECRET_REF_PREFIX);
|
|
4709
|
+
}
|
|
4710
|
+
function parseSecretRef(value) {
|
|
4711
|
+
return isSecretRef(value) ? value.slice(SECRET_REF_PREFIX.length) : null;
|
|
4712
|
+
}
|
|
4713
|
+
function collectSecretFields(schema) {
|
|
4714
|
+
const fields = schema?.fields;
|
|
4715
|
+
if (!fields) return [];
|
|
4716
|
+
const out = [];
|
|
4717
|
+
for (const [name, def] of Object.entries(fields)) {
|
|
4718
|
+
if (def && def.type === "secret") out.push(name);
|
|
4719
|
+
}
|
|
4720
|
+
return out;
|
|
4721
|
+
}
|
|
4722
|
+
|
|
4723
|
+
// src/engine.ts
|
|
4724
|
+
import { pluralToSingular, ExternalWriteForbiddenError } from "@objectstack/spec/shared";
|
|
4725
|
+
import { ExpressionEngine as ExpressionEngine3 } from "@objectstack/formula";
|
|
4726
|
+
import { isAggregatedViewContainer as isAggregatedViewContainer2, expandViewContainer } from "@objectstack/spec";
|
|
4557
4727
|
|
|
4558
4728
|
// src/hook-wrappers.ts
|
|
4559
4729
|
import { ExpressionEngine } from "@objectstack/formula";
|
|
@@ -5107,6 +5277,101 @@ function validateRecord(objectSchema, data, mode) {
|
|
|
5107
5277
|
if (errors.length > 0) throw new ValidationError(errors);
|
|
5108
5278
|
}
|
|
5109
5279
|
|
|
5280
|
+
// src/validation/rule-validator.ts
|
|
5281
|
+
import { ExpressionEngine as ExpressionEngine2 } from "@objectstack/formula";
|
|
5282
|
+
function needsPriorRecord(objectSchema) {
|
|
5283
|
+
const rules = objectSchema?.validations;
|
|
5284
|
+
if (!Array.isArray(rules)) return false;
|
|
5285
|
+
return rules.some(
|
|
5286
|
+
(r) => r != null && typeof r === "object" && (r.type === "state_machine" || r.type === "cross_field" || r.type === "script")
|
|
5287
|
+
);
|
|
5288
|
+
}
|
|
5289
|
+
function toExpression(cond) {
|
|
5290
|
+
return typeof cond === "string" ? { dialect: "cel", source: cond } : cond;
|
|
5291
|
+
}
|
|
5292
|
+
function evaluateValidationRules(objectSchema, data, mode, opts = {}) {
|
|
5293
|
+
const rules = objectSchema?.validations;
|
|
5294
|
+
if (!Array.isArray(rules) || rules.length === 0 || !data) return;
|
|
5295
|
+
const previous = opts.previous ?? void 0;
|
|
5296
|
+
const merged = { ...previous ?? {}, ...data };
|
|
5297
|
+
const errors = [];
|
|
5298
|
+
const ordered = rules.filter((r) => r != null && typeof r === "object").filter((r) => r.active !== false).filter((r) => {
|
|
5299
|
+
const events = r.events ?? ["insert", "update"];
|
|
5300
|
+
return events.includes(mode);
|
|
5301
|
+
}).sort((a, b) => (a.priority ?? 100) - (b.priority ?? 100));
|
|
5302
|
+
for (const rule of ordered) {
|
|
5303
|
+
let violation = null;
|
|
5304
|
+
try {
|
|
5305
|
+
if (rule.type === "state_machine") {
|
|
5306
|
+
violation = checkStateMachine(rule, mode, data, previous);
|
|
5307
|
+
} else if (rule.type === "script" || rule.type === "cross_field") {
|
|
5308
|
+
violation = checkPredicate(rule, merged, previous, opts.logger);
|
|
5309
|
+
}
|
|
5310
|
+
} catch (err) {
|
|
5311
|
+
opts.logger?.warn?.(`Validation rule '${rule.name}' threw \u2014 skipped`, err);
|
|
5312
|
+
continue;
|
|
5313
|
+
}
|
|
5314
|
+
if (!violation) continue;
|
|
5315
|
+
const severity = rule.severity ?? "error";
|
|
5316
|
+
if (severity === "error") {
|
|
5317
|
+
errors.push(violation);
|
|
5318
|
+
} else {
|
|
5319
|
+
opts.logger?.warn?.(
|
|
5320
|
+
`Validation rule '${rule.name}' (${severity}): ${violation.message}`
|
|
5321
|
+
);
|
|
5322
|
+
}
|
|
5323
|
+
}
|
|
5324
|
+
if (errors.length > 0) throw new ValidationError(errors);
|
|
5325
|
+
}
|
|
5326
|
+
function checkStateMachine(rule, mode, data, previous) {
|
|
5327
|
+
if (mode === "insert" || !previous) return null;
|
|
5328
|
+
if (!(rule.field in data)) return null;
|
|
5329
|
+
const from = previous[rule.field];
|
|
5330
|
+
const to = data[rule.field];
|
|
5331
|
+
if (from === to || to === void 0 || to === null) return null;
|
|
5332
|
+
const fromKey = String(from);
|
|
5333
|
+
const allowed = rule.transitions[fromKey];
|
|
5334
|
+
if (!Array.isArray(allowed)) return null;
|
|
5335
|
+
if (!allowed.includes(String(to))) {
|
|
5336
|
+
return {
|
|
5337
|
+
field: rule.field,
|
|
5338
|
+
code: "invalid_transition",
|
|
5339
|
+
message: rule.message || `Invalid transition for ${rule.field}: ${fromKey} \u2192 ${String(to)}`
|
|
5340
|
+
};
|
|
5341
|
+
}
|
|
5342
|
+
return null;
|
|
5343
|
+
}
|
|
5344
|
+
function checkPredicate(rule, record, previous, logger) {
|
|
5345
|
+
const expr = toExpression(rule.condition);
|
|
5346
|
+
const result = ExpressionEngine2.evaluate(expr, {
|
|
5347
|
+
record,
|
|
5348
|
+
previous: previous ?? void 0
|
|
5349
|
+
});
|
|
5350
|
+
if (!result.ok) {
|
|
5351
|
+
logger?.warn?.(
|
|
5352
|
+
`Validation rule '${rule.name}' predicate failed to evaluate (${result.error.kind}: ${result.error.message}) \u2014 skipped`
|
|
5353
|
+
);
|
|
5354
|
+
return null;
|
|
5355
|
+
}
|
|
5356
|
+
if (result.value === true) {
|
|
5357
|
+
return {
|
|
5358
|
+
field: rule.fields?.[0] ?? "_record",
|
|
5359
|
+
code: "rule_violation",
|
|
5360
|
+
message: rule.message
|
|
5361
|
+
};
|
|
5362
|
+
}
|
|
5363
|
+
return null;
|
|
5364
|
+
}
|
|
5365
|
+
function legalNextStates(objectSchema, field, currentState) {
|
|
5366
|
+
const rules = objectSchema?.validations;
|
|
5367
|
+
if (!Array.isArray(rules)) return null;
|
|
5368
|
+
const rule = rules.find(
|
|
5369
|
+
(r) => r != null && typeof r === "object" && r.type === "state_machine" && r.field === field
|
|
5370
|
+
);
|
|
5371
|
+
if (!rule) return null;
|
|
5372
|
+
return rule.transitions[currentState] ?? [];
|
|
5373
|
+
}
|
|
5374
|
+
|
|
5110
5375
|
// src/in-memory-aggregation.ts
|
|
5111
5376
|
function applyInMemoryAggregation(rows, ast) {
|
|
5112
5377
|
const groupBy = ast.groupBy ?? [];
|
|
@@ -5264,7 +5529,7 @@ function planFormulaProjection(schema, requestedFields) {
|
|
|
5264
5529
|
if (def?.type === "formula" && def.expression) {
|
|
5265
5530
|
const expr = typeof def.expression === "string" ? { dialect: "cel", source: def.expression } : def.expression;
|
|
5266
5531
|
plan.push({ name: f, expression: expr });
|
|
5267
|
-
|
|
5532
|
+
ExpressionEngine3.compile(expr);
|
|
5268
5533
|
} else if (Array.isArray(requestedFields) && requestedFields.length > 0) {
|
|
5269
5534
|
projected.add(f);
|
|
5270
5535
|
}
|
|
@@ -5286,7 +5551,7 @@ function applyFormulaPlan(plan, records) {
|
|
|
5286
5551
|
for (const rec of records) {
|
|
5287
5552
|
if (rec == null) continue;
|
|
5288
5553
|
for (const fp of plan) {
|
|
5289
|
-
const r =
|
|
5554
|
+
const r = ExpressionEngine3.evaluate(fp.expression, { record: rec });
|
|
5290
5555
|
rec[fp.name] = r.ok ? r.value : null;
|
|
5291
5556
|
}
|
|
5292
5557
|
}
|
|
@@ -5296,7 +5561,7 @@ function resolveMetadataItemName(key, item) {
|
|
|
5296
5561
|
if (item.name) return item.name;
|
|
5297
5562
|
if (item.id) return item.id;
|
|
5298
5563
|
if (key === "views") {
|
|
5299
|
-
return item?.list?.data?.object || item?.form?.data?.object || void 0;
|
|
5564
|
+
return item?.object || item?.list?.data?.object || item?.form?.data?.object || void 0;
|
|
5300
5565
|
}
|
|
5301
5566
|
return void 0;
|
|
5302
5567
|
}
|
|
@@ -5308,6 +5573,11 @@ var _ObjectQL = class _ObjectQL {
|
|
|
5308
5573
|
this.datasourceMapping = [];
|
|
5309
5574
|
// Package manifests registry (for defaultDatasource lookup)
|
|
5310
5575
|
this.manifests = /* @__PURE__ */ new Map();
|
|
5576
|
+
// Datasource definitions by name (ADR-0015): carries schemaMode +
|
|
5577
|
+
// external.allowWrites so the write gate (Gate 3) can enforce federation
|
|
5578
|
+
// ownership. Populated from manifests in registerApp and via
|
|
5579
|
+
// registerDatasourceDef. Absent entry ⇒ treated as managed (default DB).
|
|
5580
|
+
this.datasourceDefs = /* @__PURE__ */ new Map();
|
|
5311
5581
|
// Per-object hooks with priority support
|
|
5312
5582
|
this.hooks = /* @__PURE__ */ new Map([
|
|
5313
5583
|
["beforeFind", []],
|
|
@@ -5690,7 +5960,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
5690
5960
|
if (f.defaultValue == null) continue;
|
|
5691
5961
|
const dv = f.defaultValue;
|
|
5692
5962
|
if (typeof dv === "object" && dv !== null && dv.dialect && typeof dv.source === "string") {
|
|
5693
|
-
const result =
|
|
5963
|
+
const result = ExpressionEngine3.evaluate(dv, {
|
|
5694
5964
|
now,
|
|
5695
5965
|
user: execCtx?.userId ? { id: String(execCtx.userId), role: execCtx?.roles?.[0] } : void 0,
|
|
5696
5966
|
org: execCtx?.tenantId ? { id: String(execCtx.tenantId) } : void 0,
|
|
@@ -5730,6 +6000,12 @@ var _ObjectQL = class _ObjectQL {
|
|
|
5730
6000
|
if (id) {
|
|
5731
6001
|
this.manifests.set(id, manifest);
|
|
5732
6002
|
}
|
|
6003
|
+
if (manifest.datasources) {
|
|
6004
|
+
const dsList = Array.isArray(manifest.datasources) ? manifest.datasources : Object.entries(manifest.datasources).map(([name, def]) => ({ name, ...def }));
|
|
6005
|
+
for (const ds of dsList) {
|
|
6006
|
+
if (ds?.name) this.registerDatasourceDef(ds);
|
|
6007
|
+
}
|
|
6008
|
+
}
|
|
5733
6009
|
this._registry.installPackage(manifest);
|
|
5734
6010
|
this.logger.debug("Installed Package", { id: manifest.id, name: manifest.name, namespace });
|
|
5735
6011
|
if (manifest.objects) {
|
|
@@ -5783,6 +6059,15 @@ var _ObjectQL = class _ObjectQL {
|
|
|
5783
6059
|
this._registry.registerApp(resolved, id);
|
|
5784
6060
|
this.logger.debug("Registered manifest-as-app", { app: manifest.name, from: id });
|
|
5785
6061
|
}
|
|
6062
|
+
if (Array.isArray(manifest.navigationContributions) && manifest.navigationContributions.length > 0) {
|
|
6063
|
+
for (const contribution of manifest.navigationContributions) {
|
|
6064
|
+
this._registry.registerAppNavContribution(contribution, id);
|
|
6065
|
+
}
|
|
6066
|
+
this.logger.debug("Registered navigation contributions", {
|
|
6067
|
+
from: id,
|
|
6068
|
+
count: manifest.navigationContributions.length
|
|
6069
|
+
});
|
|
6070
|
+
}
|
|
5786
6071
|
const metadataArrayKeys = [
|
|
5787
6072
|
// UI Protocol
|
|
5788
6073
|
"actions",
|
|
@@ -5826,6 +6111,11 @@ var _ObjectQL = class _ObjectQL {
|
|
|
5826
6111
|
if (itemName) {
|
|
5827
6112
|
const toRegister = item.name === itemName ? item : { ...item, name: itemName };
|
|
5828
6113
|
this._registry.registerItem(pluralToSingular(key), toRegister, "name", id);
|
|
6114
|
+
if (key === "views" && isAggregatedViewContainer2(toRegister)) {
|
|
6115
|
+
for (const vi of expandViewContainer(itemName, toRegister)) {
|
|
6116
|
+
this._registry.registerItem("view", vi, "name", id);
|
|
6117
|
+
}
|
|
6118
|
+
}
|
|
5829
6119
|
} else {
|
|
5830
6120
|
this.logger.warn(`Skipping ${pluralToSingular(key)} without a derivable name`, { id });
|
|
5831
6121
|
}
|
|
@@ -5978,6 +6268,41 @@ var _ObjectQL = class _ObjectQL {
|
|
|
5978
6268
|
this.logger.info("Set default driver", { driverName: driver.name });
|
|
5979
6269
|
}
|
|
5980
6270
|
}
|
|
6271
|
+
/**
|
|
6272
|
+
* Register a Datasource *definition* (ADR-0015).
|
|
6273
|
+
*
|
|
6274
|
+
* Distinct from {@link registerDriver}, which registers a live connection.
|
|
6275
|
+
* This captures the declarative `schemaMode` + `external.allowWrites` so the
|
|
6276
|
+
* write gate ({@link assertWriteAllowed}) can enforce external-datasource
|
|
6277
|
+
* ownership. Safe to call repeatedly; last write wins.
|
|
6278
|
+
*/
|
|
6279
|
+
registerDatasourceDef(def) {
|
|
6280
|
+
if (!def?.name) return;
|
|
6281
|
+
this.datasourceDefs.set(def.name, { schemaMode: def.schemaMode, external: def.external });
|
|
6282
|
+
}
|
|
6283
|
+
/**
|
|
6284
|
+
* Write gate — Gate 3 of ADR-0015 §5.3.
|
|
6285
|
+
*
|
|
6286
|
+
* Blocks insert/update/delete against a federated datasource
|
|
6287
|
+
* (`schemaMode !== 'managed'`) unless BOTH the datasource opts in
|
|
6288
|
+
* (`external.allowWrites`) AND the object opts in (`external.writable`).
|
|
6289
|
+
* Managed datasources (the common case, including the absence of any
|
|
6290
|
+
* definition) are unaffected.
|
|
6291
|
+
*/
|
|
6292
|
+
assertWriteAllowed(objectName, operation) {
|
|
6293
|
+
const object = this._registry.getObject(objectName);
|
|
6294
|
+
const dsName = object?.datasource;
|
|
6295
|
+
if (!dsName || dsName === "default") return;
|
|
6296
|
+
const ds = this.datasourceDefs.get(dsName);
|
|
6297
|
+
if (!ds || !ds.schemaMode || ds.schemaMode === "managed") return;
|
|
6298
|
+
const dsAllows = ds.external?.allowWrites ?? false;
|
|
6299
|
+
const objAllows = object?.external?.writable ?? false;
|
|
6300
|
+
if (!(dsAllows && objAllows)) {
|
|
6301
|
+
throw new ExternalWriteForbiddenError(
|
|
6302
|
+
`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}).`
|
|
6303
|
+
);
|
|
6304
|
+
}
|
|
6305
|
+
}
|
|
5981
6306
|
/**
|
|
5982
6307
|
* Set the realtime service for publishing data change events.
|
|
5983
6308
|
* Should be called after kernel resolves the realtime service.
|
|
@@ -5988,6 +6313,141 @@ var _ObjectQL = class _ObjectQL {
|
|
|
5988
6313
|
this.realtimeService = service;
|
|
5989
6314
|
this.logger.info("RealtimeService configured for data events");
|
|
5990
6315
|
}
|
|
6316
|
+
/**
|
|
6317
|
+
* Register the crypto provider that backs `secret`-typed fields.
|
|
6318
|
+
*
|
|
6319
|
+
* When set, the engine encrypts secret fields on write (storing ciphertext in
|
|
6320
|
+
* `sys_secret` and only an opaque ref on the business row) and masks them on
|
|
6321
|
+
* read. When NOT set, writing to an object that declares a secret field is
|
|
6322
|
+
* **fail-closed** — the write throws rather than persist cleartext.
|
|
6323
|
+
*
|
|
6324
|
+
* Mirrors the Settings subsystem's ICryptoProvider wiring; the host (e.g.
|
|
6325
|
+
* `serve`) injects `InMemoryCryptoProvider` in dev and a KMS/Vault-backed
|
|
6326
|
+
* provider in production.
|
|
6327
|
+
*/
|
|
6328
|
+
setCryptoProvider(provider) {
|
|
6329
|
+
this.cryptoProvider = provider;
|
|
6330
|
+
this.logger.info("CryptoProvider configured for secret fields");
|
|
6331
|
+
}
|
|
6332
|
+
/**
|
|
6333
|
+
* Encrypt any `secret`-typed fields on `row` in place before it reaches the
|
|
6334
|
+
* driver. Each plaintext is wrapped by the ICryptoProvider, persisted as a
|
|
6335
|
+
* `sys_secret` row, and replaced on `row` by an opaque ref. Cleartext never
|
|
6336
|
+
* reaches the business table.
|
|
6337
|
+
*
|
|
6338
|
+
* Rules:
|
|
6339
|
+
* - No secret fields on the object ⇒ no-op (fast path, no crypto cost).
|
|
6340
|
+
* - `null`/`undefined` value ⇒ left as-is (clears the secret).
|
|
6341
|
+
* - Value already a ref (re-save of an unchanged ref) ⇒ left as-is.
|
|
6342
|
+
* - Value equal to the read mask ⇒ dropped, so a form round-trip that
|
|
6343
|
+
* echoes the mask does not overwrite the stored secret.
|
|
6344
|
+
* - **Fail-closed:** any other value with no CryptoProvider registered, or
|
|
6345
|
+
* no reachable `sys_secret` store, THROWS — never persists cleartext.
|
|
6346
|
+
*/
|
|
6347
|
+
async encryptSecretFields(object, row, context, driverOptions) {
|
|
6348
|
+
if (!row || typeof row !== "object") return;
|
|
6349
|
+
const schema = this._registry.getObject(object);
|
|
6350
|
+
const secretFields = collectSecretFields(schema);
|
|
6351
|
+
if (secretFields.length === 0) return;
|
|
6352
|
+
for (const field of secretFields) {
|
|
6353
|
+
if (!(field in row)) continue;
|
|
6354
|
+
const value = row[field];
|
|
6355
|
+
if (value === null || typeof value === "undefined") continue;
|
|
6356
|
+
if (isSecretRef(value)) continue;
|
|
6357
|
+
if (value === SECRET_MASK) {
|
|
6358
|
+
delete row[field];
|
|
6359
|
+
continue;
|
|
6360
|
+
}
|
|
6361
|
+
if (!this.cryptoProvider) {
|
|
6362
|
+
throw new Error(
|
|
6363
|
+
`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).`
|
|
6364
|
+
);
|
|
6365
|
+
}
|
|
6366
|
+
const plain = typeof value === "string" ? value : JSON.stringify(value);
|
|
6367
|
+
const handle = await this.cryptoProvider.encrypt(plain, {
|
|
6368
|
+
namespace: object,
|
|
6369
|
+
key: field,
|
|
6370
|
+
tenantId: context?.tenantId
|
|
6371
|
+
});
|
|
6372
|
+
let secretDriver;
|
|
6373
|
+
try {
|
|
6374
|
+
secretDriver = this.getDriver("sys_secret");
|
|
6375
|
+
} catch {
|
|
6376
|
+
throw new Error(
|
|
6377
|
+
`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).`
|
|
6378
|
+
);
|
|
6379
|
+
}
|
|
6380
|
+
await secretDriver.create(
|
|
6381
|
+
"sys_secret",
|
|
6382
|
+
{
|
|
6383
|
+
id: handle.id,
|
|
6384
|
+
namespace: object,
|
|
6385
|
+
key: field,
|
|
6386
|
+
kms_key_id: handle.kmsKeyId,
|
|
6387
|
+
alg: handle.alg,
|
|
6388
|
+
version: handle.version,
|
|
6389
|
+
ciphertext: handle.ciphertext,
|
|
6390
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
6391
|
+
},
|
|
6392
|
+
driverOptions
|
|
6393
|
+
);
|
|
6394
|
+
row[field] = makeSecretRef(handle.id);
|
|
6395
|
+
}
|
|
6396
|
+
}
|
|
6397
|
+
/**
|
|
6398
|
+
* Mask `secret`-typed fields on read so plaintext never leaves the engine
|
|
6399
|
+
* through the normal query path. A set secret becomes {@link SECRET_MASK};
|
|
6400
|
+
* an unset one stays `null`. Privileged callers that genuinely need the
|
|
6401
|
+
* plaintext use {@link resolveSecret} against the stored ref.
|
|
6402
|
+
*/
|
|
6403
|
+
maskSecretFields(object, rows) {
|
|
6404
|
+
if (!rows) return;
|
|
6405
|
+
const schema = this._registry.getObject(object);
|
|
6406
|
+
const secretFields = collectSecretFields(schema);
|
|
6407
|
+
if (secretFields.length === 0) return;
|
|
6408
|
+
const list = Array.isArray(rows) ? rows : [rows];
|
|
6409
|
+
for (const row of list) {
|
|
6410
|
+
if (!row || typeof row !== "object") continue;
|
|
6411
|
+
for (const field of secretFields) {
|
|
6412
|
+
if (!(field in row)) continue;
|
|
6413
|
+
row[field] = row[field] == null ? null : SECRET_MASK;
|
|
6414
|
+
}
|
|
6415
|
+
}
|
|
6416
|
+
}
|
|
6417
|
+
/**
|
|
6418
|
+
* Dereference a stored secret ref back to its plaintext. Intended for
|
|
6419
|
+
* privileged, server-side consumers (e.g. a datasource connection-pool
|
|
6420
|
+
* binder) — NOT exposed through the generic read path, which only ever
|
|
6421
|
+
* returns the mask.
|
|
6422
|
+
*
|
|
6423
|
+
* Fail-closed: throws when no CryptoProvider is registered or the
|
|
6424
|
+
* `sys_secret` row is missing. Returns `null` when `ref` is not a secret ref.
|
|
6425
|
+
*/
|
|
6426
|
+
async resolveSecret(ref, opts) {
|
|
6427
|
+
const id = parseSecretRef(ref);
|
|
6428
|
+
if (!id) return null;
|
|
6429
|
+
if (!this.cryptoProvider) {
|
|
6430
|
+
throw new Error("Cannot resolve secret: no CryptoProvider is registered (fail-closed).");
|
|
6431
|
+
}
|
|
6432
|
+
const secretDriver = this.getDriver("sys_secret");
|
|
6433
|
+
const found = await secretDriver.find("sys_secret", { object: "sys_secret", where: { id } });
|
|
6434
|
+
const secret = Array.isArray(found) ? found[0] : found;
|
|
6435
|
+
if (!secret) {
|
|
6436
|
+
throw new Error(`Cannot resolve secret: sys_secret row "${id}" not found (fail-closed).`);
|
|
6437
|
+
}
|
|
6438
|
+
const handle = {
|
|
6439
|
+
id: secret.id,
|
|
6440
|
+
kmsKeyId: secret.kms_key_id,
|
|
6441
|
+
alg: secret.alg,
|
|
6442
|
+
version: secret.version,
|
|
6443
|
+
ciphertext: secret.ciphertext
|
|
6444
|
+
};
|
|
6445
|
+
return this.cryptoProvider.decrypt(handle, {
|
|
6446
|
+
namespace: secret.namespace,
|
|
6447
|
+
key: secret.key,
|
|
6448
|
+
tenantId: opts?.tenantId
|
|
6449
|
+
});
|
|
6450
|
+
}
|
|
5991
6451
|
/**
|
|
5992
6452
|
* Helper to get object definition
|
|
5993
6453
|
*/
|
|
@@ -6280,6 +6740,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6280
6740
|
hookContext.event = "afterFind";
|
|
6281
6741
|
hookContext.result = result;
|
|
6282
6742
|
await this.triggerHooks("afterFind", hookContext);
|
|
6743
|
+
this.maskSecretFields(object, hookContext.result);
|
|
6283
6744
|
return hookContext.result;
|
|
6284
6745
|
} catch (e) {
|
|
6285
6746
|
this.logger.error("Find operation failed", e, { object });
|
|
@@ -6321,6 +6782,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6321
6782
|
const expanded = await this.expandRelatedRecords(objectName, [result], ast.expand, 0, opCtx.context);
|
|
6322
6783
|
result = expanded[0];
|
|
6323
6784
|
}
|
|
6785
|
+
this.maskSecretFields(objectName, result);
|
|
6324
6786
|
return result;
|
|
6325
6787
|
});
|
|
6326
6788
|
return opCtx.result;
|
|
@@ -6328,6 +6790,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6328
6790
|
async insert(object, data, options) {
|
|
6329
6791
|
object = this.resolveObjectName(object);
|
|
6330
6792
|
this.logger.debug("Insert operation starting", { object, isBatch: Array.isArray(data) });
|
|
6793
|
+
this.assertWriteAllowed(object, "insert");
|
|
6331
6794
|
const driver = this.getDriver(object);
|
|
6332
6795
|
const opCtx = {
|
|
6333
6796
|
object,
|
|
@@ -6356,7 +6819,13 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6356
6819
|
const rows = hookContext.input.data.map(
|
|
6357
6820
|
(row) => this.applyFieldDefaults(object, row, opCtx.context, nowSnap)
|
|
6358
6821
|
);
|
|
6359
|
-
for (const r of rows)
|
|
6822
|
+
for (const r of rows) {
|
|
6823
|
+
await this.encryptSecretFields(object, r, opCtx.context, hookContext.input.options);
|
|
6824
|
+
}
|
|
6825
|
+
for (const r of rows) {
|
|
6826
|
+
validateRecord(schemaForValidation, r, "insert");
|
|
6827
|
+
evaluateValidationRules(schemaForValidation, r, "insert", { logger: this.logger });
|
|
6828
|
+
}
|
|
6360
6829
|
if (driver.bulkCreate) {
|
|
6361
6830
|
result = await driver.bulkCreate(object, rows, hookContext.input.options);
|
|
6362
6831
|
} else {
|
|
@@ -6369,7 +6838,9 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6369
6838
|
opCtx.context,
|
|
6370
6839
|
nowSnap
|
|
6371
6840
|
);
|
|
6841
|
+
await this.encryptSecretFields(object, row, opCtx.context, hookContext.input.options);
|
|
6372
6842
|
validateRecord(schemaForValidation, row, "insert");
|
|
6843
|
+
evaluateValidationRules(schemaForValidation, row, "insert", { logger: this.logger });
|
|
6373
6844
|
result = await driver.create(object, row, hookContext.input.options);
|
|
6374
6845
|
}
|
|
6375
6846
|
hookContext.event = "afterInsert";
|
|
@@ -6419,6 +6890,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6419
6890
|
async update(object, data, options) {
|
|
6420
6891
|
object = this.resolveObjectName(object);
|
|
6421
6892
|
this.logger.debug("Update operation starting", { object });
|
|
6893
|
+
this.assertWriteAllowed(object, "update");
|
|
6422
6894
|
const driver = this.getDriver(object);
|
|
6423
6895
|
let id = data.id;
|
|
6424
6896
|
if (!id && options?.where && typeof options.where === "object" && "id" in options.where) {
|
|
@@ -6445,11 +6917,23 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6445
6917
|
hookContext.input.options = this.buildDriverOptions(opCtx.context, hookContext.input.options);
|
|
6446
6918
|
try {
|
|
6447
6919
|
let result;
|
|
6920
|
+
let priorRecord = null;
|
|
6921
|
+
const updateSchema = this._registry.getObject(object);
|
|
6448
6922
|
if (hookContext.input.id) {
|
|
6449
|
-
|
|
6923
|
+
await this.encryptSecretFields(object, hookContext.input.data, opCtx.context, hookContext.input.options);
|
|
6924
|
+
validateRecord(updateSchema, hookContext.input.data, "update");
|
|
6925
|
+
if (needsPriorRecord(updateSchema) || (this.hooks.get("afterUpdate")?.length ?? 0) > 0) {
|
|
6926
|
+
const priorAst = { object, where: { id: hookContext.input.id }, limit: 1 };
|
|
6927
|
+
priorRecord = await driver.findOne(object, priorAst, hookContext.input.options);
|
|
6928
|
+
}
|
|
6929
|
+
evaluateValidationRules(updateSchema, hookContext.input.data, "update", { previous: priorRecord, logger: this.logger });
|
|
6450
6930
|
result = await driver.update(object, hookContext.input.id, hookContext.input.data, hookContext.input.options);
|
|
6451
6931
|
} else if (options?.multi && driver.updateMany) {
|
|
6452
|
-
|
|
6932
|
+
await this.encryptSecretFields(object, hookContext.input.data, opCtx.context, hookContext.input.options);
|
|
6933
|
+
validateRecord(updateSchema, hookContext.input.data, "update");
|
|
6934
|
+
if (needsPriorRecord(updateSchema)) {
|
|
6935
|
+
this.logger.warn("Object-level validation rules (state_machine/cross_field/script) are not enforced on multi-row updates", { object });
|
|
6936
|
+
}
|
|
6453
6937
|
const ast = { object, where: options.where };
|
|
6454
6938
|
result = await driver.updateMany(object, ast, hookContext.input.data, hookContext.input.options);
|
|
6455
6939
|
} else {
|
|
@@ -6457,6 +6941,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6457
6941
|
}
|
|
6458
6942
|
hookContext.event = "afterUpdate";
|
|
6459
6943
|
hookContext.result = result;
|
|
6944
|
+
if (priorRecord) hookContext.previous = priorRecord;
|
|
6460
6945
|
await this.triggerHooks("afterUpdate", hookContext);
|
|
6461
6946
|
if (this.realtimeService) {
|
|
6462
6947
|
try {
|
|
@@ -6489,6 +6974,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6489
6974
|
async delete(object, options) {
|
|
6490
6975
|
object = this.resolveObjectName(object);
|
|
6491
6976
|
this.logger.debug("Delete operation starting", { object });
|
|
6977
|
+
this.assertWriteAllowed(object, "delete");
|
|
6492
6978
|
const driver = this.getDriver(object);
|
|
6493
6979
|
let id = void 0;
|
|
6494
6980
|
if (options?.where && typeof options.where === "object" && "id" in options.where) {
|
|
@@ -6748,6 +7234,26 @@ var _ObjectQL = class _ObjectQL {
|
|
|
6748
7234
|
getDriverByName(name) {
|
|
6749
7235
|
return this.drivers.get(name);
|
|
6750
7236
|
}
|
|
7237
|
+
/**
|
|
7238
|
+
* Introspect a datasource's live remote schema (ADR-0015).
|
|
7239
|
+
*
|
|
7240
|
+
* Resolves the driver registered under `datasource` and delegates to its
|
|
7241
|
+
* `introspectSchema()` capability. Used by the external-datasource service
|
|
7242
|
+
* (and CLI/REST) to list remote tables and validate federated objects.
|
|
7243
|
+
*
|
|
7244
|
+
* @throws if the datasource has no registered driver, or the driver does
|
|
7245
|
+
* not support introspection.
|
|
7246
|
+
*/
|
|
7247
|
+
async introspectDatasource(datasource) {
|
|
7248
|
+
const driver = this.drivers.get(datasource);
|
|
7249
|
+
if (!driver) {
|
|
7250
|
+
throw new Error(`[ObjectQL] Datasource '${datasource}' has no registered driver to introspect.`);
|
|
7251
|
+
}
|
|
7252
|
+
if (typeof driver.introspectSchema !== "function") {
|
|
7253
|
+
throw new Error(`[ObjectQL] Driver for datasource '${datasource}' does not support introspectSchema().`);
|
|
7254
|
+
}
|
|
7255
|
+
return driver.introspectSchema();
|
|
7256
|
+
}
|
|
6751
7257
|
/**
|
|
6752
7258
|
* Get the driver responsible for the given object.
|
|
6753
7259
|
*
|
|
@@ -7681,7 +8187,7 @@ var ObjectQLPlugin = class {
|
|
|
7681
8187
|
*/
|
|
7682
8188
|
async loadMetadataFromService(metadataService, ctx) {
|
|
7683
8189
|
ctx.logger.info("Syncing metadata from external service into ObjectQL registry...");
|
|
7684
|
-
const metadataTypes = ["object", "view", "app", "flow", "
|
|
8190
|
+
const metadataTypes = ["object", "view", "app", "flow", "function", "hook"];
|
|
7685
8191
|
let totalLoaded = 0;
|
|
7686
8192
|
for (const type of metadataTypes) {
|
|
7687
8193
|
try {
|
|
@@ -7831,6 +8337,8 @@ export {
|
|
|
7831
8337
|
ObjectRepository,
|
|
7832
8338
|
ObjectStackProtocolImplementation,
|
|
7833
8339
|
RESERVED_NAMESPACES,
|
|
8340
|
+
SECRET_MASK,
|
|
8341
|
+
SECRET_REF_PREFIX,
|
|
7834
8342
|
SchemaRegistry,
|
|
7835
8343
|
ScopedContext,
|
|
7836
8344
|
SysMetadataRepository,
|
|
@@ -7839,11 +8347,18 @@ export {
|
|
|
7839
8347
|
applySystemFields,
|
|
7840
8348
|
bindHooksToEngine,
|
|
7841
8349
|
bucketDateValue,
|
|
8350
|
+
collectSecretFields,
|
|
7842
8351
|
computeFQN,
|
|
7843
8352
|
convertIntrospectedSchemaToObjects,
|
|
7844
8353
|
createObjectQLKernel,
|
|
8354
|
+
evaluateValidationRules,
|
|
8355
|
+
isSecretRef,
|
|
8356
|
+
legalNextStates,
|
|
8357
|
+
makeSecretRef,
|
|
8358
|
+
needsPriorRecord,
|
|
7845
8359
|
noopHookMetricsRecorder,
|
|
7846
8360
|
parseFQN,
|
|
8361
|
+
parseSecretRef,
|
|
7847
8362
|
toTitleCase,
|
|
7848
8363
|
validateRecord,
|
|
7849
8364
|
wrapDeclarativeHook
|