@objectstack/objectql 4.0.3 → 4.0.5
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 +500 -1111
- package/dist/index.d.ts +500 -1111
- package/dist/index.js +1364 -279
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1359 -279
- package/dist/index.mjs.map +1 -1
- package/package.json +32 -6
- package/.turbo/turbo-build.log +0 -22
- package/CHANGELOG.md +0 -711
- package/src/engine.test.ts +0 -599
- package/src/engine.ts +0 -1548
- package/src/index.ts +0 -41
- package/src/kernel-factory.ts +0 -48
- package/src/metadata-facade.ts +0 -96
- package/src/plugin.integration.test.ts +0 -995
- package/src/plugin.ts +0 -534
- package/src/protocol-data.test.ts +0 -245
- package/src/protocol-discovery.test.ts +0 -213
- package/src/protocol-feed.test.ts +0 -303
- package/src/protocol-meta.test.ts +0 -440
- package/src/protocol.ts +0 -1235
- package/src/registry.test.ts +0 -494
- package/src/registry.ts +0 -716
- package/src/util.test.ts +0 -226
- package/src/util.ts +0 -219
- package/tsconfig.json +0 -10
package/dist/index.js
CHANGED
|
@@ -22,6 +22,7 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
DEFAULT_EXTENDER_PRIORITY: () => DEFAULT_EXTENDER_PRIORITY,
|
|
24
24
|
DEFAULT_OWNER_PRIORITY: () => DEFAULT_OWNER_PRIORITY,
|
|
25
|
+
InMemoryHookMetricsRecorder: () => InMemoryHookMetricsRecorder,
|
|
25
26
|
MetadataFacade: () => MetadataFacade,
|
|
26
27
|
ObjectQL: () => ObjectQL,
|
|
27
28
|
ObjectQLPlugin: () => ObjectQLPlugin,
|
|
@@ -30,11 +31,15 @@ __export(index_exports, {
|
|
|
30
31
|
RESERVED_NAMESPACES: () => RESERVED_NAMESPACES,
|
|
31
32
|
SchemaRegistry: () => SchemaRegistry,
|
|
32
33
|
ScopedContext: () => ScopedContext,
|
|
34
|
+
applySystemFields: () => applySystemFields,
|
|
35
|
+
bindHooksToEngine: () => bindHooksToEngine,
|
|
33
36
|
computeFQN: () => computeFQN,
|
|
34
37
|
convertIntrospectedSchemaToObjects: () => convertIntrospectedSchemaToObjects,
|
|
35
38
|
createObjectQLKernel: () => createObjectQLKernel,
|
|
39
|
+
noopHookMetricsRecorder: () => noopHookMetricsRecorder,
|
|
36
40
|
parseFQN: () => parseFQN,
|
|
37
|
-
toTitleCase: () => toTitleCase
|
|
41
|
+
toTitleCase: () => toTitleCase,
|
|
42
|
+
wrapDeclarativeHook: () => wrapDeclarativeHook
|
|
38
43
|
});
|
|
39
44
|
module.exports = __toCommonJS(index_exports);
|
|
40
45
|
|
|
@@ -45,11 +50,8 @@ var import_ui = require("@objectstack/spec/ui");
|
|
|
45
50
|
var RESERVED_NAMESPACES = /* @__PURE__ */ new Set(["base", "system"]);
|
|
46
51
|
var DEFAULT_OWNER_PRIORITY = 100;
|
|
47
52
|
var DEFAULT_EXTENDER_PRIORITY = 200;
|
|
48
|
-
function computeFQN(
|
|
49
|
-
|
|
50
|
-
return shortName;
|
|
51
|
-
}
|
|
52
|
-
return `${namespace}__${shortName}`;
|
|
53
|
+
function computeFQN(_namespace, shortName) {
|
|
54
|
+
return shortName;
|
|
53
55
|
}
|
|
54
56
|
function parseFQN(fqn) {
|
|
55
57
|
const idx = fqn.indexOf("__");
|
|
@@ -77,14 +79,64 @@ function mergeObjectDefinitions(base, extension) {
|
|
|
77
79
|
if (extension.description !== void 0) merged.description = extension.description;
|
|
78
80
|
return merged;
|
|
79
81
|
}
|
|
82
|
+
function applySystemFields(schema, opts) {
|
|
83
|
+
if (schema.systemFields === false) return schema;
|
|
84
|
+
if (schema.managedBy) return schema;
|
|
85
|
+
const sf = typeof schema.systemFields === "object" && schema.systemFields !== null ? schema.systemFields : void 0;
|
|
86
|
+
const wantTenant = opts.multiTenant && sf?.tenant !== false;
|
|
87
|
+
const additions = {};
|
|
88
|
+
if (wantTenant && !schema.fields?.organization_id) {
|
|
89
|
+
additions.organization_id = {
|
|
90
|
+
type: "lookup",
|
|
91
|
+
reference: "sys_organization",
|
|
92
|
+
label: "Organization",
|
|
93
|
+
required: false,
|
|
94
|
+
indexed: true,
|
|
95
|
+
hidden: true,
|
|
96
|
+
readonly: true,
|
|
97
|
+
description: "Tenant scope (auto-populated by SecurityPlugin on insert)."
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (Object.keys(additions).length === 0) return schema;
|
|
101
|
+
return {
|
|
102
|
+
...schema,
|
|
103
|
+
fields: { ...additions, ...schema.fields ?? {} }
|
|
104
|
+
};
|
|
105
|
+
}
|
|
80
106
|
var SchemaRegistry = class {
|
|
81
|
-
|
|
107
|
+
constructor(options = {}) {
|
|
108
|
+
// ==========================================
|
|
109
|
+
// Logging control
|
|
110
|
+
// ==========================================
|
|
111
|
+
/** Controls verbosity of registry console messages. Default: 'info'. */
|
|
112
|
+
this._logLevel = "info";
|
|
113
|
+
// ==========================================
|
|
114
|
+
// Object-specific storage (Ownership Model)
|
|
115
|
+
// ==========================================
|
|
116
|
+
/** FQN → Contributor[] (all packages that own/extend this object) */
|
|
117
|
+
this.objectContributors = /* @__PURE__ */ new Map();
|
|
118
|
+
/** FQN → Merged ServiceObject (cached, invalidated on changes) */
|
|
119
|
+
this.mergedObjectCache = /* @__PURE__ */ new Map();
|
|
120
|
+
/** Namespace → Set<PackageId> (multiple packages can share a namespace) */
|
|
121
|
+
this.namespaceRegistry = /* @__PURE__ */ new Map();
|
|
122
|
+
// ==========================================
|
|
123
|
+
// Generic metadata storage (non-object types)
|
|
124
|
+
// ==========================================
|
|
125
|
+
/** Type → Name/ID → MetadataItem */
|
|
126
|
+
this.metadata = /* @__PURE__ */ new Map();
|
|
127
|
+
if (options.multiTenant !== void 0) {
|
|
128
|
+
this.multiTenant = options.multiTenant;
|
|
129
|
+
} else {
|
|
130
|
+
this.multiTenant = String(process.env.OS_MULTI_TENANT ?? "true").toLowerCase() !== "false";
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
get logLevel() {
|
|
82
134
|
return this._logLevel;
|
|
83
135
|
}
|
|
84
|
-
|
|
136
|
+
set logLevel(level) {
|
|
85
137
|
this._logLevel = level;
|
|
86
138
|
}
|
|
87
|
-
|
|
139
|
+
log(msg) {
|
|
88
140
|
if (this._logLevel === "silent" || this._logLevel === "error" || this._logLevel === "warn") return;
|
|
89
141
|
console.log(msg);
|
|
90
142
|
}
|
|
@@ -95,7 +147,7 @@ var SchemaRegistry = class {
|
|
|
95
147
|
* Register a namespace for a package.
|
|
96
148
|
* Multiple packages can share the same namespace (e.g. 'sys').
|
|
97
149
|
*/
|
|
98
|
-
|
|
150
|
+
registerNamespace(namespace, packageId) {
|
|
99
151
|
if (!namespace) return;
|
|
100
152
|
let owners = this.namespaceRegistry.get(namespace);
|
|
101
153
|
if (!owners) {
|
|
@@ -108,7 +160,7 @@ var SchemaRegistry = class {
|
|
|
108
160
|
/**
|
|
109
161
|
* Unregister a namespace when a package is uninstalled.
|
|
110
162
|
*/
|
|
111
|
-
|
|
163
|
+
unregisterNamespace(namespace, packageId) {
|
|
112
164
|
const owners = this.namespaceRegistry.get(namespace);
|
|
113
165
|
if (owners) {
|
|
114
166
|
owners.delete(packageId);
|
|
@@ -121,7 +173,7 @@ var SchemaRegistry = class {
|
|
|
121
173
|
/**
|
|
122
174
|
* Get the packages that use a namespace.
|
|
123
175
|
*/
|
|
124
|
-
|
|
176
|
+
getNamespaceOwner(namespace) {
|
|
125
177
|
const owners = this.namespaceRegistry.get(namespace);
|
|
126
178
|
if (!owners || owners.size === 0) return void 0;
|
|
127
179
|
return owners.values().next().value;
|
|
@@ -129,7 +181,7 @@ var SchemaRegistry = class {
|
|
|
129
181
|
/**
|
|
130
182
|
* Get all packages that share a namespace.
|
|
131
183
|
*/
|
|
132
|
-
|
|
184
|
+
getNamespaceOwners(namespace) {
|
|
133
185
|
const owners = this.namespaceRegistry.get(namespace);
|
|
134
186
|
return owners ? Array.from(owners) : [];
|
|
135
187
|
}
|
|
@@ -147,7 +199,8 @@ var SchemaRegistry = class {
|
|
|
147
199
|
*
|
|
148
200
|
* @throws Error if trying to 'own' an object that already has an owner
|
|
149
201
|
*/
|
|
150
|
-
|
|
202
|
+
registerObject(schema, packageId, namespace, ownership = "own", priority = ownership === "own" ? DEFAULT_OWNER_PRIORITY : DEFAULT_EXTENDER_PRIORITY) {
|
|
203
|
+
schema = applySystemFields(schema, { multiTenant: this.multiTenant });
|
|
151
204
|
const shortName = schema.name;
|
|
152
205
|
const fqn = computeFQN(namespace, shortName);
|
|
153
206
|
if (namespace) {
|
|
@@ -194,7 +247,7 @@ var SchemaRegistry = class {
|
|
|
194
247
|
* Resolve an object by FQN, merging all contributions.
|
|
195
248
|
* Returns the merged object or undefined if not found.
|
|
196
249
|
*/
|
|
197
|
-
|
|
250
|
+
resolveObject(fqn) {
|
|
198
251
|
const cached = this.mergedObjectCache.get(fqn);
|
|
199
252
|
if (cached) return cached;
|
|
200
253
|
const contributors = this.objectContributors.get(fqn);
|
|
@@ -216,38 +269,42 @@ var SchemaRegistry = class {
|
|
|
216
269
|
return merged;
|
|
217
270
|
}
|
|
218
271
|
/**
|
|
219
|
-
* Get object by name (
|
|
272
|
+
* Get object by name (short name canonical, FQN supported for disambiguation).
|
|
273
|
+
*
|
|
274
|
+
* Short names are canonical for user code, AI generation, and most lookups.
|
|
275
|
+
* FQN is accepted as an explicit fallback for cross-package disambiguation
|
|
276
|
+
* when two packages contribute objects with the same short name.
|
|
220
277
|
*
|
|
221
278
|
* Resolution order:
|
|
222
|
-
* 1. Exact
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const direct = this.resolveObject(name);
|
|
230
|
-
if (direct) return direct;
|
|
279
|
+
* 1. Exact name match — the name IS the canonical key.
|
|
280
|
+
* If multiple packages contribute the same short name, a warning is logged
|
|
281
|
+
* and the first match wins — disambiguate by passing the FQN explicitly.
|
|
282
|
+
* 2. Legacy FQN match (e.g., 'crm__account') — backward compat.
|
|
283
|
+
*/
|
|
284
|
+
getObject(name) {
|
|
285
|
+
const matches = [];
|
|
231
286
|
for (const fqn of this.objectContributors.keys()) {
|
|
232
287
|
const { shortName } = parseFQN(fqn);
|
|
233
288
|
if (shortName === name) {
|
|
234
|
-
|
|
289
|
+
matches.push(fqn);
|
|
235
290
|
}
|
|
236
291
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
292
|
+
if (matches.length > 0) {
|
|
293
|
+
if (matches.length > 1) {
|
|
294
|
+
console.warn(
|
|
295
|
+
`[SchemaRegistry] Ambiguous short name "${name}" matches: ${matches.join(", ")}. Returning first match. Use FQN to disambiguate.`
|
|
296
|
+
);
|
|
241
297
|
}
|
|
298
|
+
return this.resolveObject(matches[0]);
|
|
242
299
|
}
|
|
243
|
-
return
|
|
300
|
+
return this.resolveObject(name);
|
|
244
301
|
}
|
|
245
302
|
/**
|
|
246
303
|
* Get all registered objects (merged).
|
|
247
304
|
*
|
|
248
305
|
* @param packageId - Optional filter: only objects contributed by this package
|
|
249
306
|
*/
|
|
250
|
-
|
|
307
|
+
getAllObjects(packageId) {
|
|
251
308
|
const results = [];
|
|
252
309
|
for (const fqn of this.objectContributors.keys()) {
|
|
253
310
|
if (packageId) {
|
|
@@ -266,13 +323,13 @@ var SchemaRegistry = class {
|
|
|
266
323
|
/**
|
|
267
324
|
* Get all contributors for an object.
|
|
268
325
|
*/
|
|
269
|
-
|
|
326
|
+
getObjectContributors(fqn) {
|
|
270
327
|
return this.objectContributors.get(fqn) || [];
|
|
271
328
|
}
|
|
272
329
|
/**
|
|
273
330
|
* Get the owner contributor for an object.
|
|
274
331
|
*/
|
|
275
|
-
|
|
332
|
+
getObjectOwner(fqn) {
|
|
276
333
|
const contributors = this.objectContributors.get(fqn);
|
|
277
334
|
return contributors?.find((c) => c.ownership === "own");
|
|
278
335
|
}
|
|
@@ -281,7 +338,7 @@ var SchemaRegistry = class {
|
|
|
281
338
|
*
|
|
282
339
|
* @throws Error if trying to uninstall an owner that has extenders
|
|
283
340
|
*/
|
|
284
|
-
|
|
341
|
+
unregisterObjectsByPackage(packageId, force = false) {
|
|
285
342
|
for (const [fqn, contributors] of this.objectContributors.entries()) {
|
|
286
343
|
const packageContribs = contributors.filter((c) => c.packageId === packageId);
|
|
287
344
|
for (const contrib of packageContribs) {
|
|
@@ -313,7 +370,7 @@ var SchemaRegistry = class {
|
|
|
313
370
|
/**
|
|
314
371
|
* Universal Register Method for non-object metadata.
|
|
315
372
|
*/
|
|
316
|
-
|
|
373
|
+
registerItem(type, item, keyField = "name", packageId) {
|
|
317
374
|
if (!this.metadata.has(type)) {
|
|
318
375
|
this.metadata.set(type, /* @__PURE__ */ new Map());
|
|
319
376
|
}
|
|
@@ -329,7 +386,7 @@ var SchemaRegistry = class {
|
|
|
329
386
|
}
|
|
330
387
|
const storageKey = packageId ? `${packageId}:${baseName}` : baseName;
|
|
331
388
|
if (collection.has(storageKey)) {
|
|
332
|
-
|
|
389
|
+
this.log(`[Registry] Overwriting ${type}: ${storageKey}`);
|
|
333
390
|
}
|
|
334
391
|
collection.set(storageKey, item);
|
|
335
392
|
this.log(`[Registry] Registered ${type}: ${storageKey}`);
|
|
@@ -337,7 +394,7 @@ var SchemaRegistry = class {
|
|
|
337
394
|
/**
|
|
338
395
|
* Validate Metadata against Spec Zod Schemas
|
|
339
396
|
*/
|
|
340
|
-
|
|
397
|
+
validate(type, item) {
|
|
341
398
|
if (type === "object") {
|
|
342
399
|
return import_data.ObjectSchema.parse(item);
|
|
343
400
|
}
|
|
@@ -355,7 +412,7 @@ var SchemaRegistry = class {
|
|
|
355
412
|
/**
|
|
356
413
|
* Universal Unregister Method
|
|
357
414
|
*/
|
|
358
|
-
|
|
415
|
+
unregisterItem(type, name) {
|
|
359
416
|
const collection = this.metadata.get(type);
|
|
360
417
|
if (!collection) {
|
|
361
418
|
console.warn(`[Registry] Attempted to unregister non-existent ${type}: ${name}`);
|
|
@@ -378,7 +435,7 @@ var SchemaRegistry = class {
|
|
|
378
435
|
/**
|
|
379
436
|
* Universal Get Method
|
|
380
437
|
*/
|
|
381
|
-
|
|
438
|
+
getItem(type, name) {
|
|
382
439
|
if (type === "object" || type === "objects") {
|
|
383
440
|
return this.getObject(name);
|
|
384
441
|
}
|
|
@@ -394,7 +451,7 @@ var SchemaRegistry = class {
|
|
|
394
451
|
/**
|
|
395
452
|
* Universal List Method
|
|
396
453
|
*/
|
|
397
|
-
|
|
454
|
+
listItems(type, packageId) {
|
|
398
455
|
if (type === "object" || type === "objects") {
|
|
399
456
|
return this.getAllObjects(packageId);
|
|
400
457
|
}
|
|
@@ -407,7 +464,7 @@ var SchemaRegistry = class {
|
|
|
407
464
|
/**
|
|
408
465
|
* Get all registered metadata types (Kinds)
|
|
409
466
|
*/
|
|
410
|
-
|
|
467
|
+
getRegisteredTypes() {
|
|
411
468
|
const types = Array.from(this.metadata.keys());
|
|
412
469
|
if (!types.includes("object") && this.objectContributors.size > 0) {
|
|
413
470
|
types.push("object");
|
|
@@ -417,7 +474,7 @@ var SchemaRegistry = class {
|
|
|
417
474
|
// ==========================================
|
|
418
475
|
// Package Management
|
|
419
476
|
// ==========================================
|
|
420
|
-
|
|
477
|
+
installPackage(manifest, settings) {
|
|
421
478
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
422
479
|
const pkg = {
|
|
423
480
|
manifest,
|
|
@@ -441,7 +498,7 @@ var SchemaRegistry = class {
|
|
|
441
498
|
this.log(`[Registry] Installed package: ${manifest.id} (${manifest.name})`);
|
|
442
499
|
return pkg;
|
|
443
500
|
}
|
|
444
|
-
|
|
501
|
+
uninstallPackage(id) {
|
|
445
502
|
const pkg = this.getPackage(id);
|
|
446
503
|
if (!pkg) {
|
|
447
504
|
console.warn(`[Registry] Package not found for uninstall: ${id}`);
|
|
@@ -459,13 +516,13 @@ var SchemaRegistry = class {
|
|
|
459
516
|
}
|
|
460
517
|
return false;
|
|
461
518
|
}
|
|
462
|
-
|
|
519
|
+
getPackage(id) {
|
|
463
520
|
return this.metadata.get("package")?.get(id);
|
|
464
521
|
}
|
|
465
|
-
|
|
522
|
+
getAllPackages() {
|
|
466
523
|
return this.listItems("package");
|
|
467
524
|
}
|
|
468
|
-
|
|
525
|
+
enablePackage(id) {
|
|
469
526
|
const pkg = this.getPackage(id);
|
|
470
527
|
if (pkg) {
|
|
471
528
|
pkg.enabled = true;
|
|
@@ -476,7 +533,7 @@ var SchemaRegistry = class {
|
|
|
476
533
|
}
|
|
477
534
|
return pkg;
|
|
478
535
|
}
|
|
479
|
-
|
|
536
|
+
disablePackage(id) {
|
|
480
537
|
const pkg = this.getPackage(id);
|
|
481
538
|
if (pkg) {
|
|
482
539
|
pkg.enabled = false;
|
|
@@ -490,31 +547,31 @@ var SchemaRegistry = class {
|
|
|
490
547
|
// ==========================================
|
|
491
548
|
// App Helpers
|
|
492
549
|
// ==========================================
|
|
493
|
-
|
|
550
|
+
registerApp(app, packageId) {
|
|
494
551
|
this.registerItem("app", app, "name", packageId);
|
|
495
552
|
}
|
|
496
|
-
|
|
553
|
+
getApp(name) {
|
|
497
554
|
return this.getItem("app", name);
|
|
498
555
|
}
|
|
499
|
-
|
|
556
|
+
getAllApps() {
|
|
500
557
|
return this.listItems("app");
|
|
501
558
|
}
|
|
502
559
|
// ==========================================
|
|
503
560
|
// Plugin Helpers
|
|
504
561
|
// ==========================================
|
|
505
|
-
|
|
562
|
+
registerPlugin(manifest) {
|
|
506
563
|
this.registerItem("plugin", manifest, "id");
|
|
507
564
|
}
|
|
508
|
-
|
|
565
|
+
getAllPlugins() {
|
|
509
566
|
return this.listItems("plugin");
|
|
510
567
|
}
|
|
511
568
|
// ==========================================
|
|
512
569
|
// Kind Helpers
|
|
513
570
|
// ==========================================
|
|
514
|
-
|
|
571
|
+
registerKind(kind) {
|
|
515
572
|
this.registerItem("kind", kind, "id");
|
|
516
573
|
}
|
|
517
|
-
|
|
574
|
+
getAllKinds() {
|
|
518
575
|
return this.listItems("kind");
|
|
519
576
|
}
|
|
520
577
|
// ==========================================
|
|
@@ -523,7 +580,7 @@ var SchemaRegistry = class {
|
|
|
523
580
|
/**
|
|
524
581
|
* Clear all registry state. Use only for testing.
|
|
525
582
|
*/
|
|
526
|
-
|
|
583
|
+
reset() {
|
|
527
584
|
this.objectContributors.clear();
|
|
528
585
|
this.mergedObjectCache.clear();
|
|
529
586
|
this.namespaceRegistry.clear();
|
|
@@ -531,25 +588,6 @@ var SchemaRegistry = class {
|
|
|
531
588
|
this.log("[Registry] Reset complete");
|
|
532
589
|
}
|
|
533
590
|
};
|
|
534
|
-
// ==========================================
|
|
535
|
-
// Logging control
|
|
536
|
-
// ==========================================
|
|
537
|
-
/** Controls verbosity of registry console messages. Default: 'info'. */
|
|
538
|
-
SchemaRegistry._logLevel = "info";
|
|
539
|
-
// ==========================================
|
|
540
|
-
// Object-specific storage (Ownership Model)
|
|
541
|
-
// ==========================================
|
|
542
|
-
/** FQN → Contributor[] (all packages that own/extend this object) */
|
|
543
|
-
SchemaRegistry.objectContributors = /* @__PURE__ */ new Map();
|
|
544
|
-
/** FQN → Merged ServiceObject (cached, invalidated on changes) */
|
|
545
|
-
SchemaRegistry.mergedObjectCache = /* @__PURE__ */ new Map();
|
|
546
|
-
/** Namespace → Set<PackageId> (multiple packages can share a namespace) */
|
|
547
|
-
SchemaRegistry.namespaceRegistry = /* @__PURE__ */ new Map();
|
|
548
|
-
// ==========================================
|
|
549
|
-
// Generic metadata storage (non-object types)
|
|
550
|
-
// ==========================================
|
|
551
|
-
/** Type → Name/ID → MetadataItem */
|
|
552
|
-
SchemaRegistry.metadata = /* @__PURE__ */ new Map();
|
|
553
591
|
|
|
554
592
|
// src/protocol.ts
|
|
555
593
|
var import_data2 = require("@objectstack/spec/data");
|
|
@@ -581,10 +619,20 @@ var SERVICE_CONFIG = {
|
|
|
581
619
|
search: { route: "/api/v1/search", plugin: "plugin-search" }
|
|
582
620
|
};
|
|
583
621
|
var ObjectStackProtocolImplementation = class {
|
|
584
|
-
constructor(engine, getServicesRegistry, getFeedService) {
|
|
622
|
+
constructor(engine, getServicesRegistry, getFeedService, projectId) {
|
|
585
623
|
this.engine = engine;
|
|
586
624
|
this.getServicesRegistry = getServicesRegistry;
|
|
587
625
|
this.getFeedService = getFeedService;
|
|
626
|
+
this.projectId = projectId;
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Exposes the project scope the protocol is bound to. Consumers like
|
|
630
|
+
* the HTTP dispatcher use this to decide whether to trust the process-
|
|
631
|
+
* wide SchemaRegistry or whether they must route a read through the
|
|
632
|
+
* protocol's project_id-filtered lookup.
|
|
633
|
+
*/
|
|
634
|
+
getProjectId() {
|
|
635
|
+
return this.projectId;
|
|
588
636
|
}
|
|
589
637
|
requireFeedService() {
|
|
590
638
|
const svc = this.getFeedService?.();
|
|
@@ -681,7 +729,7 @@ var ObjectStackProtocolImplementation = class {
|
|
|
681
729
|
};
|
|
682
730
|
}
|
|
683
731
|
async getMetaTypes() {
|
|
684
|
-
const schemaTypes =
|
|
732
|
+
const schemaTypes = this.engine.registry.getRegisteredTypes();
|
|
685
733
|
let runtimeTypes = [];
|
|
686
734
|
try {
|
|
687
735
|
const services = this.getServicesRegistry?.();
|
|
@@ -696,47 +744,67 @@ var ObjectStackProtocolImplementation = class {
|
|
|
696
744
|
}
|
|
697
745
|
async getMetaItems(request) {
|
|
698
746
|
const { packageId } = request;
|
|
699
|
-
let items =
|
|
700
|
-
if (
|
|
701
|
-
|
|
702
|
-
if (
|
|
747
|
+
let items = [];
|
|
748
|
+
if (this.projectId === void 0) {
|
|
749
|
+
items = [...this.engine.registry.listItems(request.type, packageId)];
|
|
750
|
+
if (items.length === 0) {
|
|
751
|
+
const alt = import_shared.PLURAL_TO_SINGULAR[request.type] ?? import_shared.SINGULAR_TO_PLURAL[request.type];
|
|
752
|
+
if (alt) items = [...this.engine.registry.listItems(alt, packageId)];
|
|
753
|
+
}
|
|
754
|
+
} else {
|
|
755
|
+
items = [...this.engine.registry.listItems(request.type, packageId)];
|
|
756
|
+
if (items.length === 0) {
|
|
757
|
+
const alt = import_shared.PLURAL_TO_SINGULAR[request.type] ?? import_shared.SINGULAR_TO_PLURAL[request.type];
|
|
758
|
+
if (alt) items = [...this.engine.registry.listItems(alt, packageId)];
|
|
759
|
+
}
|
|
703
760
|
}
|
|
704
|
-
if (
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
});
|
|
729
|
-
}
|
|
761
|
+
if (this.projectId === void 0) try {
|
|
762
|
+
const whereClause = {
|
|
763
|
+
type: request.type,
|
|
764
|
+
state: "active",
|
|
765
|
+
// Always filter by project_id: project kernels use their projectId,
|
|
766
|
+
// control-plane kernels use NULL (global scope only).
|
|
767
|
+
project_id: this.projectId ?? null
|
|
768
|
+
};
|
|
769
|
+
if (packageId) whereClause._packageId = packageId;
|
|
770
|
+
let records = await this.engine.find("sys_metadata", { where: whereClause });
|
|
771
|
+
if (!records || records.length === 0) {
|
|
772
|
+
const alt = import_shared.PLURAL_TO_SINGULAR[request.type] ?? import_shared.SINGULAR_TO_PLURAL[request.type];
|
|
773
|
+
if (alt) {
|
|
774
|
+
const altWhere = { type: alt, state: "active", project_id: this.projectId ?? null };
|
|
775
|
+
if (packageId) altWhere._packageId = packageId;
|
|
776
|
+
records = await this.engine.find("sys_metadata", { where: altWhere });
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
if (records && records.length > 0) {
|
|
780
|
+
const byName = /* @__PURE__ */ new Map();
|
|
781
|
+
for (const existing of items) {
|
|
782
|
+
const entry = existing;
|
|
783
|
+
if (entry && typeof entry === "object" && "name" in entry) {
|
|
784
|
+
byName.set(entry.name, entry);
|
|
730
785
|
}
|
|
731
786
|
}
|
|
732
|
-
|
|
787
|
+
for (const record of records) {
|
|
788
|
+
const data = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
|
|
789
|
+
if (data && typeof data === "object" && "name" in data) {
|
|
790
|
+
byName.set(data.name, data);
|
|
791
|
+
}
|
|
792
|
+
if (this.projectId === void 0) {
|
|
793
|
+
this.engine.registry.registerItem(request.type, data, "name");
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
items = Array.from(byName.values());
|
|
733
797
|
}
|
|
798
|
+
} catch {
|
|
734
799
|
}
|
|
735
800
|
try {
|
|
736
801
|
const services = this.getServicesRegistry?.();
|
|
737
802
|
const metadataService = services?.get("metadata");
|
|
738
803
|
if (metadataService && typeof metadataService.list === "function") {
|
|
739
|
-
|
|
804
|
+
let runtimeItems = await metadataService.list(request.type);
|
|
805
|
+
if (packageId && runtimeItems && runtimeItems.length > 0) {
|
|
806
|
+
runtimeItems = runtimeItems.filter((item) => item?._packageId === packageId);
|
|
807
|
+
}
|
|
740
808
|
if (runtimeItems && runtimeItems.length > 0) {
|
|
741
809
|
const itemMap = /* @__PURE__ */ new Map();
|
|
742
810
|
for (const item of items) {
|
|
@@ -762,28 +830,41 @@ var ObjectStackProtocolImplementation = class {
|
|
|
762
830
|
};
|
|
763
831
|
}
|
|
764
832
|
async getMetaItem(request) {
|
|
765
|
-
let item
|
|
766
|
-
if (
|
|
767
|
-
|
|
768
|
-
if (
|
|
833
|
+
let item;
|
|
834
|
+
if (this.projectId === void 0) {
|
|
835
|
+
item = this.engine.registry.getItem(request.type, request.name);
|
|
836
|
+
if (item === void 0) {
|
|
837
|
+
const alt = import_shared.PLURAL_TO_SINGULAR[request.type] ?? import_shared.SINGULAR_TO_PLURAL[request.type];
|
|
838
|
+
if (alt) item = this.engine.registry.getItem(alt, request.name);
|
|
839
|
+
}
|
|
769
840
|
}
|
|
770
|
-
if (item === void 0) {
|
|
841
|
+
if (item === void 0 && this.projectId === void 0) {
|
|
771
842
|
try {
|
|
843
|
+
const scopedWhere = {
|
|
844
|
+
type: request.type,
|
|
845
|
+
name: request.name,
|
|
846
|
+
state: "active"
|
|
847
|
+
};
|
|
848
|
+
scopedWhere.project_id = this.projectId ?? null;
|
|
772
849
|
const record = await this.engine.findOne("sys_metadata", {
|
|
773
|
-
where:
|
|
850
|
+
where: scopedWhere
|
|
774
851
|
});
|
|
775
852
|
if (record) {
|
|
776
853
|
item = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
|
|
777
|
-
|
|
854
|
+
if (this.projectId === void 0) {
|
|
855
|
+
this.engine.registry.registerItem(request.type, item, "name");
|
|
856
|
+
}
|
|
778
857
|
} else {
|
|
779
858
|
const alt = import_shared.PLURAL_TO_SINGULAR[request.type] ?? import_shared.SINGULAR_TO_PLURAL[request.type];
|
|
780
859
|
if (alt) {
|
|
781
|
-
const
|
|
782
|
-
|
|
783
|
-
});
|
|
860
|
+
const altWhere = { type: alt, name: request.name, state: "active" };
|
|
861
|
+
altWhere.project_id = this.projectId ?? null;
|
|
862
|
+
const altRecord = await this.engine.findOne("sys_metadata", { where: altWhere });
|
|
784
863
|
if (altRecord) {
|
|
785
864
|
item = typeof altRecord.metadata === "string" ? JSON.parse(altRecord.metadata) : altRecord.metadata;
|
|
786
|
-
|
|
865
|
+
if (this.projectId === void 0) {
|
|
866
|
+
this.engine.registry.registerItem(request.type, item, "name");
|
|
867
|
+
}
|
|
787
868
|
}
|
|
788
869
|
}
|
|
789
870
|
}
|
|
@@ -807,7 +888,7 @@ var ObjectStackProtocolImplementation = class {
|
|
|
807
888
|
};
|
|
808
889
|
}
|
|
809
890
|
async getUiView(request) {
|
|
810
|
-
const schema =
|
|
891
|
+
const schema = this.engine.registry.getObject(request.object);
|
|
811
892
|
if (!schema) throw new Error(`Object ${request.object} not found`);
|
|
812
893
|
const fields = schema.fields || {};
|
|
813
894
|
const fieldKeys = Object.keys(fields);
|
|
@@ -863,6 +944,9 @@ var ObjectStackProtocolImplementation = class {
|
|
|
863
944
|
}
|
|
864
945
|
async findData(request) {
|
|
865
946
|
const options = { ...request.query };
|
|
947
|
+
if (request.context !== void 0) {
|
|
948
|
+
options.context = request.context;
|
|
949
|
+
}
|
|
866
950
|
if (options.top != null) {
|
|
867
951
|
options.limit = Number(options.top);
|
|
868
952
|
delete options.top;
|
|
@@ -981,6 +1065,9 @@ var ObjectStackProtocolImplementation = class {
|
|
|
981
1065
|
const queryOptions = {
|
|
982
1066
|
where: { id: request.id }
|
|
983
1067
|
};
|
|
1068
|
+
if (request.context !== void 0) {
|
|
1069
|
+
queryOptions.context = request.context;
|
|
1070
|
+
}
|
|
984
1071
|
if (request.select) {
|
|
985
1072
|
queryOptions.fields = typeof request.select === "string" ? request.select.split(",").map((s) => s.trim()).filter(Boolean) : request.select;
|
|
986
1073
|
}
|
|
@@ -1002,7 +1089,11 @@ var ObjectStackProtocolImplementation = class {
|
|
|
1002
1089
|
throw new Error(`Record ${request.id} not found in ${request.object}`);
|
|
1003
1090
|
}
|
|
1004
1091
|
async createData(request) {
|
|
1005
|
-
const result = await this.engine.insert(
|
|
1092
|
+
const result = await this.engine.insert(
|
|
1093
|
+
request.object,
|
|
1094
|
+
request.data,
|
|
1095
|
+
request.context !== void 0 ? { context: request.context } : void 0
|
|
1096
|
+
);
|
|
1006
1097
|
return {
|
|
1007
1098
|
object: request.object,
|
|
1008
1099
|
id: result.id,
|
|
@@ -1010,7 +1101,9 @@ var ObjectStackProtocolImplementation = class {
|
|
|
1010
1101
|
};
|
|
1011
1102
|
}
|
|
1012
1103
|
async updateData(request) {
|
|
1013
|
-
const
|
|
1104
|
+
const opts = { where: { id: request.id } };
|
|
1105
|
+
if (request.context !== void 0) opts.context = request.context;
|
|
1106
|
+
const result = await this.engine.update(request.object, request.data, opts);
|
|
1014
1107
|
return {
|
|
1015
1108
|
object: request.object,
|
|
1016
1109
|
id: request.id,
|
|
@@ -1018,7 +1111,9 @@ var ObjectStackProtocolImplementation = class {
|
|
|
1018
1111
|
};
|
|
1019
1112
|
}
|
|
1020
1113
|
async deleteData(request) {
|
|
1021
|
-
|
|
1114
|
+
const opts = { where: { id: request.id } };
|
|
1115
|
+
if (request.context !== void 0) opts.context = request.context;
|
|
1116
|
+
await this.engine.delete(request.object, opts);
|
|
1022
1117
|
return {
|
|
1023
1118
|
object: request.object,
|
|
1024
1119
|
id: request.id,
|
|
@@ -1030,10 +1125,10 @@ var ObjectStackProtocolImplementation = class {
|
|
|
1030
1125
|
// ==========================================
|
|
1031
1126
|
async getMetaItemCached(request) {
|
|
1032
1127
|
try {
|
|
1033
|
-
let item =
|
|
1128
|
+
let item = this.engine.registry.getItem(request.type, request.name);
|
|
1034
1129
|
if (!item) {
|
|
1035
1130
|
const alt = import_shared.PLURAL_TO_SINGULAR[request.type] ?? import_shared.SINGULAR_TO_PLURAL[request.type];
|
|
1036
|
-
if (alt) item =
|
|
1131
|
+
if (alt) item = this.engine.registry.getItem(alt, request.name);
|
|
1037
1132
|
}
|
|
1038
1133
|
if (!item) {
|
|
1039
1134
|
try {
|
|
@@ -1236,7 +1331,7 @@ var ObjectStackProtocolImplementation = class {
|
|
|
1236
1331
|
};
|
|
1237
1332
|
}
|
|
1238
1333
|
async getAnalyticsMeta(request) {
|
|
1239
|
-
const objects =
|
|
1334
|
+
const objects = this.engine.registry.listItems("object");
|
|
1240
1335
|
const cubeFilter = request?.cube;
|
|
1241
1336
|
const cubes = [];
|
|
1242
1337
|
for (const obj of objects) {
|
|
@@ -1340,11 +1435,43 @@ var ObjectStackProtocolImplementation = class {
|
|
|
1340
1435
|
if (!request.item) {
|
|
1341
1436
|
throw new Error("Item data is required");
|
|
1342
1437
|
}
|
|
1343
|
-
|
|
1438
|
+
if (this.projectId !== void 0) {
|
|
1439
|
+
this.engine.registry.registerItem(request.type, request.item, "name");
|
|
1440
|
+
if (request.type === "object" || request.type === "objects") {
|
|
1441
|
+
try {
|
|
1442
|
+
this.engine.registry.registerObject(request.item, "sys_metadata");
|
|
1443
|
+
} catch (err) {
|
|
1444
|
+
console.warn(
|
|
1445
|
+
`[Protocol] registerObject failed for ${request.name}: ${err?.message ?? err}`
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
return {
|
|
1450
|
+
success: true,
|
|
1451
|
+
message: "Saved to memory registry (project kernel \u2014 sys_metadata is control-plane only)"
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
this.engine.registry.registerItem(request.type, request.item, "name");
|
|
1455
|
+
if (request.type === "object" || request.type === "objects") {
|
|
1456
|
+
try {
|
|
1457
|
+
this.engine.registry.registerObject(request.item, "sys_metadata");
|
|
1458
|
+
} catch (err) {
|
|
1459
|
+
console.warn(
|
|
1460
|
+
`[Protocol] this.engine.registry.registerObject failed for ${request.name}: ${err?.message ?? err}`
|
|
1461
|
+
);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1344
1464
|
try {
|
|
1345
1465
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1466
|
+
const scopedWhere = {
|
|
1467
|
+
type: request.type,
|
|
1468
|
+
name: request.name
|
|
1469
|
+
};
|
|
1470
|
+
if (this.projectId !== void 0) {
|
|
1471
|
+
scopedWhere.project_id = this.projectId;
|
|
1472
|
+
}
|
|
1346
1473
|
const existing = await this.engine.findOne("sys_metadata", {
|
|
1347
|
-
where:
|
|
1474
|
+
where: scopedWhere
|
|
1348
1475
|
});
|
|
1349
1476
|
if (existing) {
|
|
1350
1477
|
await this.engine.update("sys_metadata", {
|
|
@@ -1356,17 +1483,24 @@ var ObjectStackProtocolImplementation = class {
|
|
|
1356
1483
|
});
|
|
1357
1484
|
} else {
|
|
1358
1485
|
const id = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `meta_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
1359
|
-
|
|
1486
|
+
const row = {
|
|
1360
1487
|
id,
|
|
1361
1488
|
name: request.name,
|
|
1362
1489
|
type: request.type,
|
|
1363
|
-
scope
|
|
1490
|
+
// `scope` tracks platform vs project authorship. With
|
|
1491
|
+
// project_id carries the project id, 'project' is the
|
|
1492
|
+
// honest label whenever we know we're inside one.
|
|
1493
|
+
scope: this.projectId !== void 0 ? "project" : "platform",
|
|
1364
1494
|
metadata: JSON.stringify(request.item),
|
|
1365
1495
|
state: "active",
|
|
1366
1496
|
version: 1,
|
|
1367
1497
|
created_at: now,
|
|
1368
1498
|
updated_at: now
|
|
1369
|
-
}
|
|
1499
|
+
};
|
|
1500
|
+
if (this.projectId !== void 0) {
|
|
1501
|
+
row.project_id = this.projectId;
|
|
1502
|
+
}
|
|
1503
|
+
await this.engine.insert("sys_metadata", row);
|
|
1370
1504
|
}
|
|
1371
1505
|
return {
|
|
1372
1506
|
success: true,
|
|
@@ -1387,20 +1521,25 @@ var ObjectStackProtocolImplementation = class {
|
|
|
1387
1521
|
* Safe to call repeatedly — idempotent (latest DB record wins).
|
|
1388
1522
|
*/
|
|
1389
1523
|
async loadMetaFromDb() {
|
|
1524
|
+
if (this.projectId !== void 0) {
|
|
1525
|
+
return { loaded: 0, errors: 0 };
|
|
1526
|
+
}
|
|
1390
1527
|
let loaded = 0;
|
|
1391
1528
|
let errors = 0;
|
|
1392
1529
|
try {
|
|
1393
|
-
const
|
|
1394
|
-
|
|
1395
|
-
|
|
1530
|
+
const where = {
|
|
1531
|
+
state: "active",
|
|
1532
|
+
project_id: this.projectId ?? null
|
|
1533
|
+
};
|
|
1534
|
+
const records = await this.engine.find("sys_metadata", { where });
|
|
1396
1535
|
for (const record of records) {
|
|
1397
1536
|
try {
|
|
1398
1537
|
const data = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
|
|
1399
1538
|
const normalizedType = import_shared.PLURAL_TO_SINGULAR[record.type] ?? record.type;
|
|
1400
1539
|
if (normalizedType === "object") {
|
|
1401
|
-
|
|
1540
|
+
this.engine.registry.registerObject(data, record.packageId || "sys_metadata");
|
|
1402
1541
|
} else {
|
|
1403
|
-
|
|
1542
|
+
this.engine.registry.registerItem(normalizedType, data, "name");
|
|
1404
1543
|
}
|
|
1405
1544
|
loaded++;
|
|
1406
1545
|
} catch (e) {
|
|
@@ -1409,7 +1548,9 @@ var ObjectStackProtocolImplementation = class {
|
|
|
1409
1548
|
}
|
|
1410
1549
|
}
|
|
1411
1550
|
} catch (e) {
|
|
1412
|
-
|
|
1551
|
+
if (!/no such table/i.test(e.message ?? "")) {
|
|
1552
|
+
console.warn(`[Protocol] DB hydration skipped: ${e.message}`);
|
|
1553
|
+
}
|
|
1413
1554
|
}
|
|
1414
1555
|
return { loaded, errors };
|
|
1415
1556
|
}
|
|
@@ -1551,10 +1692,488 @@ var import_kernel2 = require("@objectstack/spec/kernel");
|
|
|
1551
1692
|
var import_core = require("@objectstack/core");
|
|
1552
1693
|
var import_system = require("@objectstack/spec/system");
|
|
1553
1694
|
var import_shared2 = require("@objectstack/spec/shared");
|
|
1695
|
+
var import_formula2 = require("@objectstack/formula");
|
|
1696
|
+
|
|
1697
|
+
// src/hook-wrappers.ts
|
|
1698
|
+
var import_formula = require("@objectstack/formula");
|
|
1699
|
+
|
|
1700
|
+
// src/hook-metrics.ts
|
|
1701
|
+
var noopHookMetricsRecorder = {
|
|
1702
|
+
recordExecution: () => {
|
|
1703
|
+
},
|
|
1704
|
+
recordSkip: () => {
|
|
1705
|
+
},
|
|
1706
|
+
recordRetry: () => {
|
|
1707
|
+
}
|
|
1708
|
+
};
|
|
1709
|
+
var InMemoryHookMetricsRecorder = class {
|
|
1710
|
+
constructor() {
|
|
1711
|
+
this.executions = /* @__PURE__ */ new Map();
|
|
1712
|
+
this.skips = /* @__PURE__ */ new Map();
|
|
1713
|
+
this.retries = /* @__PURE__ */ new Map();
|
|
1714
|
+
}
|
|
1715
|
+
recordExecution(label, outcome, durationMs) {
|
|
1716
|
+
const key = `${label.hook}|${outcome}`;
|
|
1717
|
+
const cur = this.executions.get(key) ?? { count: 0, totalMs: 0 };
|
|
1718
|
+
cur.count += 1;
|
|
1719
|
+
cur.totalMs += Math.max(0, durationMs);
|
|
1720
|
+
this.executions.set(key, cur);
|
|
1721
|
+
}
|
|
1722
|
+
recordSkip(label, reason) {
|
|
1723
|
+
const key = `${label.hook}|${reason}`;
|
|
1724
|
+
this.skips.set(key, (this.skips.get(key) ?? 0) + 1);
|
|
1725
|
+
}
|
|
1726
|
+
recordRetry(label, _attempt) {
|
|
1727
|
+
this.retries.set(label.hook, (this.retries.get(label.hook) ?? 0) + 1);
|
|
1728
|
+
}
|
|
1729
|
+
snapshot() {
|
|
1730
|
+
return {
|
|
1731
|
+
executions: Array.from(this.executions, ([key, v]) => {
|
|
1732
|
+
const [hook, outcome] = key.split("|");
|
|
1733
|
+
return { hook, outcome, count: v.count, totalMs: v.totalMs };
|
|
1734
|
+
}),
|
|
1735
|
+
skips: Array.from(this.skips, ([key, count]) => {
|
|
1736
|
+
const [hook, reason] = key.split("|");
|
|
1737
|
+
return { hook, reason, count };
|
|
1738
|
+
}),
|
|
1739
|
+
retries: Array.from(this.retries, ([hook, count]) => ({ hook, count }))
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
reset() {
|
|
1743
|
+
this.executions.clear();
|
|
1744
|
+
this.skips.clear();
|
|
1745
|
+
this.retries.clear();
|
|
1746
|
+
}
|
|
1747
|
+
};
|
|
1748
|
+
|
|
1749
|
+
// src/hook-wrappers.ts
|
|
1750
|
+
var noopLogger = {
|
|
1751
|
+
debug: () => {
|
|
1752
|
+
},
|
|
1753
|
+
info: () => {
|
|
1754
|
+
},
|
|
1755
|
+
warn: () => {
|
|
1756
|
+
},
|
|
1757
|
+
error: () => {
|
|
1758
|
+
}
|
|
1759
|
+
};
|
|
1760
|
+
function wrapDeclarativeHook(meta, handler, opts = {}) {
|
|
1761
|
+
const logger = opts.logger ?? noopLogger;
|
|
1762
|
+
const metrics = opts.metrics ?? noopHookMetricsRecorder;
|
|
1763
|
+
const isAfterEvent = meta.events?.some((e) => typeof e === "string" && e.startsWith("after")) ?? false;
|
|
1764
|
+
const hasBody = Boolean(meta.body);
|
|
1765
|
+
const labelFor = (ctx) => ({
|
|
1766
|
+
hook: meta.name,
|
|
1767
|
+
object: ctx.object ?? (typeof meta.object === "string" ? meta.object : void 0),
|
|
1768
|
+
event: ctx.event,
|
|
1769
|
+
body: hasBody
|
|
1770
|
+
});
|
|
1771
|
+
let conditionFn;
|
|
1772
|
+
if (meta.condition) {
|
|
1773
|
+
const expr = typeof meta.condition === "string" ? { dialect: "cel", source: meta.condition } : meta.condition;
|
|
1774
|
+
if (expr.source && expr.source.trim()) {
|
|
1775
|
+
const check = import_formula.ExpressionEngine.compile(expr);
|
|
1776
|
+
if (check.ok) {
|
|
1777
|
+
conditionFn = (record) => {
|
|
1778
|
+
const r = import_formula.ExpressionEngine.evaluate(expr, { record: record ?? {} });
|
|
1779
|
+
if (!r.ok) {
|
|
1780
|
+
logger.warn("[hook] condition evaluation failed; treating as false", {
|
|
1781
|
+
hook: meta.name,
|
|
1782
|
+
condition: expr.source,
|
|
1783
|
+
error: r.error.message
|
|
1784
|
+
});
|
|
1785
|
+
return false;
|
|
1786
|
+
}
|
|
1787
|
+
return Boolean(r.value);
|
|
1788
|
+
};
|
|
1789
|
+
} else {
|
|
1790
|
+
logger.warn("[hook] condition formula failed to compile; condition ignored", {
|
|
1791
|
+
hook: meta.name,
|
|
1792
|
+
condition: expr.source,
|
|
1793
|
+
error: check.error.message
|
|
1794
|
+
});
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
const retryMax = Math.max(0, Number(meta.retryPolicy?.maxRetries ?? 0));
|
|
1799
|
+
const retryBackoffMs = Math.max(0, Number(meta.retryPolicy?.backoffMs ?? 0));
|
|
1800
|
+
const timeoutMs = typeof meta.timeout === "number" && meta.timeout > 0 ? meta.timeout : void 0;
|
|
1801
|
+
const onError = meta.onError ?? "abort";
|
|
1802
|
+
const fireAndForget = Boolean(meta.async) && isAfterEvent;
|
|
1803
|
+
const runWithTimeout = async (ctx) => {
|
|
1804
|
+
if (!timeoutMs) {
|
|
1805
|
+
await handler(ctx);
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1808
|
+
let timer;
|
|
1809
|
+
try {
|
|
1810
|
+
await Promise.race([
|
|
1811
|
+
Promise.resolve().then(() => handler(ctx)),
|
|
1812
|
+
new Promise((_, reject) => {
|
|
1813
|
+
timer = setTimeout(() => {
|
|
1814
|
+
reject(new Error(`Hook '${meta.name}' timed out after ${timeoutMs}ms`));
|
|
1815
|
+
}, timeoutMs);
|
|
1816
|
+
})
|
|
1817
|
+
]);
|
|
1818
|
+
} finally {
|
|
1819
|
+
if (timer) clearTimeout(timer);
|
|
1820
|
+
}
|
|
1821
|
+
};
|
|
1822
|
+
const runWithRetry = async (ctx) => {
|
|
1823
|
+
let attempt = 0;
|
|
1824
|
+
let lastErr;
|
|
1825
|
+
while (attempt <= retryMax) {
|
|
1826
|
+
try {
|
|
1827
|
+
await runWithTimeout(ctx);
|
|
1828
|
+
return;
|
|
1829
|
+
} catch (err) {
|
|
1830
|
+
lastErr = err;
|
|
1831
|
+
attempt += 1;
|
|
1832
|
+
if (attempt > retryMax) break;
|
|
1833
|
+
if (retryBackoffMs > 0) {
|
|
1834
|
+
await new Promise((r) => setTimeout(r, retryBackoffMs * attempt));
|
|
1835
|
+
}
|
|
1836
|
+
try {
|
|
1837
|
+
metrics.recordRetry(labelFor(ctx), attempt);
|
|
1838
|
+
} catch {
|
|
1839
|
+
}
|
|
1840
|
+
logger.warn("[hook] retrying after failure", {
|
|
1841
|
+
hook: meta.name,
|
|
1842
|
+
attempt,
|
|
1843
|
+
maxRetries: retryMax,
|
|
1844
|
+
error: err?.message
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
throw lastErr;
|
|
1849
|
+
};
|
|
1850
|
+
const runWithErrorPolicy = async (ctx) => {
|
|
1851
|
+
try {
|
|
1852
|
+
await runWithRetry(ctx);
|
|
1853
|
+
} catch (err) {
|
|
1854
|
+
if (onError === "log") {
|
|
1855
|
+
logger.error("[hook] handler failed (onError=log; suppressing)", {
|
|
1856
|
+
hook: meta.name,
|
|
1857
|
+
object: ctx.object,
|
|
1858
|
+
event: ctx.event,
|
|
1859
|
+
error: err?.message
|
|
1860
|
+
});
|
|
1861
|
+
return;
|
|
1862
|
+
}
|
|
1863
|
+
throw err;
|
|
1864
|
+
}
|
|
1865
|
+
};
|
|
1866
|
+
return async (ctx) => {
|
|
1867
|
+
if (conditionFn) {
|
|
1868
|
+
const record = pickRecordPayload(ctx);
|
|
1869
|
+
if (!conditionFn(record)) {
|
|
1870
|
+
logger.debug("[hook] skipped by condition", {
|
|
1871
|
+
hook: meta.name,
|
|
1872
|
+
object: ctx.object,
|
|
1873
|
+
event: ctx.event
|
|
1874
|
+
});
|
|
1875
|
+
try {
|
|
1876
|
+
metrics.recordSkip(labelFor(ctx), "condition");
|
|
1877
|
+
} catch {
|
|
1878
|
+
}
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
const restore = installFlatInput(ctx);
|
|
1883
|
+
const startedAt = Date.now();
|
|
1884
|
+
const recordOutcome = (err) => {
|
|
1885
|
+
const elapsed = Date.now() - startedAt;
|
|
1886
|
+
let outcome = "success";
|
|
1887
|
+
if (err) {
|
|
1888
|
+
const msg = String(err?.message ?? err ?? "");
|
|
1889
|
+
if (/timed out after/i.test(msg)) outcome = "timeout";
|
|
1890
|
+
else if (/capability|cap-rejection|capability_rejected/i.test(msg)) outcome = "capability_rejected";
|
|
1891
|
+
else outcome = "error";
|
|
1892
|
+
}
|
|
1893
|
+
try {
|
|
1894
|
+
metrics.recordExecution(labelFor(ctx), outcome, elapsed);
|
|
1895
|
+
} catch {
|
|
1896
|
+
}
|
|
1897
|
+
};
|
|
1898
|
+
try {
|
|
1899
|
+
if (fireAndForget) {
|
|
1900
|
+
try {
|
|
1901
|
+
metrics.recordSkip(labelFor(ctx), "fire_and_forget");
|
|
1902
|
+
} catch {
|
|
1903
|
+
}
|
|
1904
|
+
void runWithErrorPolicy(ctx).then(() => recordOutcome()).catch((err) => {
|
|
1905
|
+
recordOutcome(err);
|
|
1906
|
+
logger.error("[hook] async handler error (fire-and-forget)", {
|
|
1907
|
+
hook: meta.name,
|
|
1908
|
+
error: err?.message
|
|
1909
|
+
});
|
|
1910
|
+
});
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1913
|
+
try {
|
|
1914
|
+
await runWithErrorPolicy(ctx);
|
|
1915
|
+
recordOutcome();
|
|
1916
|
+
} catch (err) {
|
|
1917
|
+
recordOutcome(err);
|
|
1918
|
+
throw err;
|
|
1919
|
+
}
|
|
1920
|
+
} finally {
|
|
1921
|
+
restore();
|
|
1922
|
+
}
|
|
1923
|
+
};
|
|
1924
|
+
}
|
|
1925
|
+
function installFlatInput(ctx) {
|
|
1926
|
+
const raw = ctx.input ?? {};
|
|
1927
|
+
const looksWrapped = raw && typeof raw === "object" && ("data" in raw || "options" in raw || "id" in raw || "ast" in raw);
|
|
1928
|
+
if (!looksWrapped) return () => {
|
|
1929
|
+
};
|
|
1930
|
+
const ensureData = () => {
|
|
1931
|
+
if (!raw.data || typeof raw.data !== "object") {
|
|
1932
|
+
raw.data = {};
|
|
1933
|
+
}
|
|
1934
|
+
return raw.data;
|
|
1935
|
+
};
|
|
1936
|
+
const proxy = new Proxy(raw, {
|
|
1937
|
+
get(target, prop, receiver) {
|
|
1938
|
+
if (prop === "id" || prop === "options" || prop === "ast" || prop === "data") {
|
|
1939
|
+
return Reflect.get(target, prop, receiver);
|
|
1940
|
+
}
|
|
1941
|
+
const data = target.data;
|
|
1942
|
+
if (data && typeof data === "object" && prop in data) {
|
|
1943
|
+
return data[prop];
|
|
1944
|
+
}
|
|
1945
|
+
return Reflect.get(target, prop, receiver);
|
|
1946
|
+
},
|
|
1947
|
+
set(target, prop, value) {
|
|
1948
|
+
if (prop === "id" || prop === "options" || prop === "ast" || prop === "data") {
|
|
1949
|
+
target[prop] = value;
|
|
1950
|
+
return true;
|
|
1951
|
+
}
|
|
1952
|
+
ensureData()[prop] = value;
|
|
1953
|
+
return true;
|
|
1954
|
+
},
|
|
1955
|
+
has(target, prop) {
|
|
1956
|
+
if (prop === "id" || prop === "options" || prop === "ast" || prop === "data") {
|
|
1957
|
+
return prop in target;
|
|
1958
|
+
}
|
|
1959
|
+
const data = target.data;
|
|
1960
|
+
if (data && typeof data === "object" && prop in data) return true;
|
|
1961
|
+
return prop in target;
|
|
1962
|
+
},
|
|
1963
|
+
ownKeys(target) {
|
|
1964
|
+
const dataKeys = target.data && typeof target.data === "object" ? Object.keys(target.data) : [];
|
|
1965
|
+
return Array.from(new Set(dataKeys));
|
|
1966
|
+
},
|
|
1967
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
1968
|
+
const data = target.data;
|
|
1969
|
+
if (data && typeof data === "object" && prop in data) {
|
|
1970
|
+
return { configurable: true, enumerable: true, writable: true, value: data[prop] };
|
|
1971
|
+
}
|
|
1972
|
+
if (prop === "id" || prop === "options" || prop === "ast" || prop === "data") {
|
|
1973
|
+
const desc = Object.getOwnPropertyDescriptor(target, prop);
|
|
1974
|
+
return desc ? { ...desc, enumerable: false } : void 0;
|
|
1975
|
+
}
|
|
1976
|
+
return Object.getOwnPropertyDescriptor(target, prop);
|
|
1977
|
+
}
|
|
1978
|
+
});
|
|
1979
|
+
ctx.input = proxy;
|
|
1980
|
+
return () => {
|
|
1981
|
+
ctx.input = raw;
|
|
1982
|
+
};
|
|
1983
|
+
}
|
|
1984
|
+
function pickRecordPayload(ctx) {
|
|
1985
|
+
const input = ctx.input ?? {};
|
|
1986
|
+
if (input && typeof input === "object" && input.data && typeof input.data === "object") {
|
|
1987
|
+
return input.data;
|
|
1988
|
+
}
|
|
1989
|
+
if (ctx.previous && typeof ctx.previous === "object") {
|
|
1990
|
+
return ctx.previous;
|
|
1991
|
+
}
|
|
1992
|
+
return input;
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
// src/hook-binder.ts
|
|
1996
|
+
var noopLogger2 = {
|
|
1997
|
+
debug: () => {
|
|
1998
|
+
},
|
|
1999
|
+
info: () => {
|
|
2000
|
+
},
|
|
2001
|
+
warn: () => {
|
|
2002
|
+
},
|
|
2003
|
+
error: () => {
|
|
2004
|
+
}
|
|
2005
|
+
};
|
|
2006
|
+
function bindHooksToEngine(engine, hooks, opts = {}) {
|
|
2007
|
+
const logger = opts.logger ?? noopLogger2;
|
|
2008
|
+
const result = { registered: 0, skipped: 0, errors: [] };
|
|
2009
|
+
if (!Array.isArray(hooks) || hooks.length === 0) {
|
|
2010
|
+
return result;
|
|
2011
|
+
}
|
|
2012
|
+
if (opts.packageId && typeof engine.unregisterHooksByPackage === "function") {
|
|
2013
|
+
try {
|
|
2014
|
+
engine.unregisterHooksByPackage(opts.packageId);
|
|
2015
|
+
} catch (err) {
|
|
2016
|
+
logger.warn("[hook-binder] unregister-by-package failed; continuing", {
|
|
2017
|
+
packageId: opts.packageId,
|
|
2018
|
+
error: err?.message
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
if (opts.functions && typeof engine.registerFunction === "function") {
|
|
2023
|
+
for (const [name, fn] of Object.entries(opts.functions)) {
|
|
2024
|
+
try {
|
|
2025
|
+
engine.registerFunction(name, fn, opts.packageId);
|
|
2026
|
+
} catch (err) {
|
|
2027
|
+
logger.warn("[hook-binder] failed to register function", {
|
|
2028
|
+
name,
|
|
2029
|
+
error: err?.message
|
|
2030
|
+
});
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
for (const hook of hooks) {
|
|
2035
|
+
try {
|
|
2036
|
+
const resolved = resolveHandler(engine, hook, opts);
|
|
2037
|
+
if (!resolved) {
|
|
2038
|
+
result.skipped += 1;
|
|
2039
|
+
const reason = hook.body ? `hook body present but no bodyRunner supplied to bindHooksToEngine (runtime must wire QuickJSScriptRunner)` : typeof hook.handler === "string" ? `unknown function '${hook.handler}'` : "no handler";
|
|
2040
|
+
result.errors.push({ hook: hook.name, reason });
|
|
2041
|
+
if (opts.strict) {
|
|
2042
|
+
throw new Error(`[hook-binder] strict: cannot bind hook '${hook.name}': ${reason}`);
|
|
2043
|
+
}
|
|
2044
|
+
logger.warn("[hook-binder] skipping hook with unresolved handler", {
|
|
2045
|
+
hook: hook.name,
|
|
2046
|
+
handler: hook.handler,
|
|
2047
|
+
hasBody: Boolean(hook.body)
|
|
2048
|
+
});
|
|
2049
|
+
continue;
|
|
2050
|
+
}
|
|
2051
|
+
if (opts.warnLegacyHandler && !hook.body && typeof hook.handler === "string") {
|
|
2052
|
+
logger.warn("[hook-binder] DEPRECATED: hook uses legacy handler ref without body", {
|
|
2053
|
+
hook: hook.name,
|
|
2054
|
+
handler: hook.handler,
|
|
2055
|
+
hint: "Move the handler source into Hook.body so the artifact stays metadata-only and the .mjs runtime bundle can be dropped."
|
|
2056
|
+
});
|
|
2057
|
+
}
|
|
2058
|
+
const wrapped = wrapDeclarativeHook(hook, resolved, { logger, metrics: opts.metrics });
|
|
2059
|
+
const objects = normalizeObjects(hook.object);
|
|
2060
|
+
const events = Array.isArray(hook.events) ? hook.events : [];
|
|
2061
|
+
for (const event of events) {
|
|
2062
|
+
for (const object of objects) {
|
|
2063
|
+
engine.registerHook(event, wrapped, {
|
|
2064
|
+
object,
|
|
2065
|
+
priority: typeof hook.priority === "number" ? hook.priority : 100,
|
|
2066
|
+
packageId: opts.packageId,
|
|
2067
|
+
// Reflect metadata so future tooling can introspect / unregister
|
|
2068
|
+
// and so we can detect duplicate name collisions.
|
|
2069
|
+
// The engine ignores unknown options today; this is forward-only.
|
|
2070
|
+
...{ meta: hook, hookName: hook.name }
|
|
2071
|
+
});
|
|
2072
|
+
result.registered += 1;
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
} catch (err) {
|
|
2076
|
+
result.errors.push({ hook: hook.name, reason: err?.message ?? String(err) });
|
|
2077
|
+
logger.error("[hook-binder] failed to bind hook", {
|
|
2078
|
+
hook: hook.name,
|
|
2079
|
+
error: err?.message
|
|
2080
|
+
});
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
if (result.registered > 0) {
|
|
2084
|
+
logger.debug("[hook-binder] hooks bound", {
|
|
2085
|
+
packageId: opts.packageId,
|
|
2086
|
+
registered: result.registered,
|
|
2087
|
+
skipped: result.skipped
|
|
2088
|
+
});
|
|
2089
|
+
}
|
|
2090
|
+
return result;
|
|
2091
|
+
}
|
|
2092
|
+
function normalizeObjects(target) {
|
|
2093
|
+
if (Array.isArray(target)) return target.length > 0 ? target : ["*"];
|
|
2094
|
+
if (typeof target === "string" && target.length > 0) return [target];
|
|
2095
|
+
return ["*"];
|
|
2096
|
+
}
|
|
2097
|
+
function resolveHandler(engine, hook, opts) {
|
|
2098
|
+
const body = hook.body;
|
|
2099
|
+
if (body && typeof body === "object") {
|
|
2100
|
+
let runner = opts.bodyRunner;
|
|
2101
|
+
if (typeof runner !== "function") {
|
|
2102
|
+
const fallback = engine?._defaultBodyRunner;
|
|
2103
|
+
if (typeof fallback === "function") runner = fallback;
|
|
2104
|
+
}
|
|
2105
|
+
if (typeof runner !== "function") {
|
|
2106
|
+
return void 0;
|
|
2107
|
+
}
|
|
2108
|
+
const fn = runner(hook);
|
|
2109
|
+
if (typeof fn === "function") return fn;
|
|
2110
|
+
return void 0;
|
|
2111
|
+
}
|
|
2112
|
+
const h = hook.handler;
|
|
2113
|
+
if (typeof h === "function") return h;
|
|
2114
|
+
if (typeof h === "string" && h.length > 0) {
|
|
2115
|
+
const fromBundle = opts.functions?.[h];
|
|
2116
|
+
if (typeof fromBundle === "function") return fromBundle;
|
|
2117
|
+
if (typeof engine.resolveFunction === "function") {
|
|
2118
|
+
const fn = engine.resolveFunction(h);
|
|
2119
|
+
if (typeof fn === "function") return fn;
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
return void 0;
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
// src/engine.ts
|
|
2126
|
+
function planFormulaProjection(schema, requestedFields) {
|
|
2127
|
+
if (!schema?.fields) return { plan: [] };
|
|
2128
|
+
const allFieldNames = Object.keys(schema.fields);
|
|
2129
|
+
const targets = Array.isArray(requestedFields) && requestedFields.length > 0 ? requestedFields : allFieldNames;
|
|
2130
|
+
const plan = [];
|
|
2131
|
+
const projected = /* @__PURE__ */ new Set();
|
|
2132
|
+
for (const f of targets) {
|
|
2133
|
+
const def = schema.fields[f];
|
|
2134
|
+
if (def?.type === "formula" && def.expression) {
|
|
2135
|
+
const expr = typeof def.expression === "string" ? { dialect: "cel", source: def.expression } : def.expression;
|
|
2136
|
+
plan.push({ name: f, expression: expr });
|
|
2137
|
+
import_formula2.ExpressionEngine.compile(expr);
|
|
2138
|
+
} else if (Array.isArray(requestedFields) && requestedFields.length > 0) {
|
|
2139
|
+
projected.add(f);
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
if (plan.length === 0) return { plan: [] };
|
|
2143
|
+
if (Array.isArray(requestedFields) && requestedFields.length > 0) {
|
|
2144
|
+
if (!projected.has("id")) projected.add("id");
|
|
2145
|
+
for (const fname of allFieldNames) projected.add(fname);
|
|
2146
|
+
return { plan, projected: Array.from(projected) };
|
|
2147
|
+
}
|
|
2148
|
+
return { plan };
|
|
2149
|
+
}
|
|
2150
|
+
function applyFormulaPlan(plan, records) {
|
|
2151
|
+
if (!plan.length) return;
|
|
2152
|
+
for (const rec of records) {
|
|
2153
|
+
if (rec == null) continue;
|
|
2154
|
+
for (const fp of plan) {
|
|
2155
|
+
const r = import_formula2.ExpressionEngine.evaluate(fp.expression, { record: rec });
|
|
2156
|
+
rec[fp.name] = r.ok ? r.value : null;
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
function resolveMetadataItemName(key, item) {
|
|
2161
|
+
if (!item) return void 0;
|
|
2162
|
+
if (item.name) return item.name;
|
|
2163
|
+
if (item.id) return item.id;
|
|
2164
|
+
if (key === "views") {
|
|
2165
|
+
return item?.list?.data?.object || item?.form?.data?.object || void 0;
|
|
2166
|
+
}
|
|
2167
|
+
return void 0;
|
|
2168
|
+
}
|
|
1554
2169
|
var _ObjectQL = class _ObjectQL {
|
|
1555
2170
|
constructor(hostContext = {}) {
|
|
1556
2171
|
this.drivers = /* @__PURE__ */ new Map();
|
|
1557
2172
|
this.defaultDriver = null;
|
|
2173
|
+
// Datasource mapping rules (imported from defineStack)
|
|
2174
|
+
this.datasourceMapping = [];
|
|
2175
|
+
// Package manifests registry (for defaultDatasource lookup)
|
|
2176
|
+
this.manifests = /* @__PURE__ */ new Map();
|
|
1558
2177
|
// Per-object hooks with priority support
|
|
1559
2178
|
this.hooks = /* @__PURE__ */ new Map([
|
|
1560
2179
|
["beforeFind", []],
|
|
@@ -1570,10 +2189,29 @@ var _ObjectQL = class _ObjectQL {
|
|
|
1570
2189
|
this.middlewares = [];
|
|
1571
2190
|
// Action registry: key = "objectName:actionName"
|
|
1572
2191
|
this.actions = /* @__PURE__ */ new Map();
|
|
2192
|
+
// Function registry: name → handler. Used by `bindHooksToEngine` to
|
|
2193
|
+
// resolve string-named hook handlers (the JSON-safe form). Populated by
|
|
2194
|
+
// `defineStack({ functions })` via `AppPlugin`, or directly via
|
|
2195
|
+
// `engine.registerFunction(...)`.
|
|
2196
|
+
this.functions = /* @__PURE__ */ new Map();
|
|
1573
2197
|
// Host provided context additions (e.g. Server router)
|
|
1574
2198
|
this.hostContext = {};
|
|
2199
|
+
// Per-engine SchemaRegistry instance.
|
|
2200
|
+
//
|
|
2201
|
+
// Historically SchemaRegistry was a process-wide singleton of static state,
|
|
2202
|
+
// which broke multi-project servers: a project kernel would inherit every
|
|
2203
|
+
// object registered by the control plane (e.g. sys_metadata), and
|
|
2204
|
+
// getDriver()'s owner lookup would route CRUD to the wrong database. Each
|
|
2205
|
+
// engine now owns its registry so kernels are fully isolated.
|
|
2206
|
+
this._registry = new SchemaRegistry();
|
|
1575
2207
|
this.hostContext = hostContext;
|
|
1576
2208
|
this.logger = hostContext.logger || (0, import_core.createLogger)({ level: "info", format: "pretty" });
|
|
2209
|
+
if (process?.env?.OBJECTQL_STRICT_HOOKS === "1") {
|
|
2210
|
+
this._strictHookBinding = true;
|
|
2211
|
+
}
|
|
2212
|
+
if (process?.env?.OBJECTQL_WARN_LEGACY_HANDLER === "1") {
|
|
2213
|
+
this._warnLegacyHandler = true;
|
|
2214
|
+
}
|
|
1577
2215
|
this.logger.info("ObjectQL Engine Instance Created");
|
|
1578
2216
|
}
|
|
1579
2217
|
/**
|
|
@@ -1589,10 +2227,13 @@ var _ObjectQL = class _ObjectQL {
|
|
|
1589
2227
|
};
|
|
1590
2228
|
}
|
|
1591
2229
|
/**
|
|
1592
|
-
* Expose the SchemaRegistry for plugins to register metadata
|
|
2230
|
+
* Expose the SchemaRegistry for plugins to register metadata.
|
|
2231
|
+
*
|
|
2232
|
+
* Returns the per-engine instance, NOT the class. Each ObjectQL engine
|
|
2233
|
+
* owns its registry so multi-project kernels remain isolated.
|
|
1593
2234
|
*/
|
|
1594
2235
|
get registry() {
|
|
1595
|
-
return
|
|
2236
|
+
return this._registry;
|
|
1596
2237
|
}
|
|
1597
2238
|
/**
|
|
1598
2239
|
* Load and Register a Plugin
|
|
@@ -1638,11 +2279,121 @@ var _ObjectQL = class _ObjectQL {
|
|
|
1638
2279
|
handler,
|
|
1639
2280
|
object: options?.object,
|
|
1640
2281
|
priority: options?.priority ?? 100,
|
|
1641
|
-
packageId: options?.packageId
|
|
2282
|
+
packageId: options?.packageId,
|
|
2283
|
+
meta: options?.meta,
|
|
2284
|
+
hookName: options?.hookName
|
|
1642
2285
|
});
|
|
1643
2286
|
entries.sort((a, b) => a.priority - b.priority);
|
|
1644
2287
|
this.logger.debug("Registered hook", { event, object: options?.object, priority: options?.priority ?? 100, totalHandlers: entries.length });
|
|
1645
2288
|
}
|
|
2289
|
+
/**
|
|
2290
|
+
* Remove all hooks registered under a given `packageId`. Used by
|
|
2291
|
+
* `bindHooksToEngine` to make re-binding (hot reload, app reinstall)
|
|
2292
|
+
* idempotent, and by app uninstall flows.
|
|
2293
|
+
*/
|
|
2294
|
+
unregisterHooksByPackage(packageId) {
|
|
2295
|
+
if (!packageId) return 0;
|
|
2296
|
+
let removed = 0;
|
|
2297
|
+
for (const [event, entries] of this.hooks.entries()) {
|
|
2298
|
+
const before = entries.length;
|
|
2299
|
+
const kept = entries.filter((e) => e.packageId !== packageId);
|
|
2300
|
+
if (kept.length !== before) {
|
|
2301
|
+
this.hooks.set(event, kept);
|
|
2302
|
+
removed += before - kept.length;
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
if (removed > 0) {
|
|
2306
|
+
this.logger.debug("Unregistered hooks by package", { packageId, removed });
|
|
2307
|
+
}
|
|
2308
|
+
return removed;
|
|
2309
|
+
}
|
|
2310
|
+
/**
|
|
2311
|
+
* Register a named function handler that can later be referenced by
|
|
2312
|
+
* string from a `Hook.handler` field. This is the JSON-safe form of
|
|
2313
|
+
* handler binding — declarative metadata persisted to disk or shipped
|
|
2314
|
+
* over the wire only carries the name.
|
|
2315
|
+
*/
|
|
2316
|
+
registerFunction(name, handler, packageId) {
|
|
2317
|
+
if (!name || typeof handler !== "function") return;
|
|
2318
|
+
this.functions.set(name, { handler, packageId });
|
|
2319
|
+
this.logger.debug("Registered function", { name, packageId });
|
|
2320
|
+
}
|
|
2321
|
+
/** Look up a registered function by name. */
|
|
2322
|
+
resolveFunction(name) {
|
|
2323
|
+
return this.functions.get(name)?.handler;
|
|
2324
|
+
}
|
|
2325
|
+
/** Remove all functions registered under a given `packageId`. */
|
|
2326
|
+
unregisterFunctionsByPackage(packageId) {
|
|
2327
|
+
if (!packageId) return 0;
|
|
2328
|
+
let removed = 0;
|
|
2329
|
+
for (const [name, entry] of this.functions.entries()) {
|
|
2330
|
+
if (entry.packageId === packageId) {
|
|
2331
|
+
this.functions.delete(name);
|
|
2332
|
+
removed += 1;
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
if (removed > 0) {
|
|
2336
|
+
this.logger.debug("Unregistered functions by package", { packageId, removed });
|
|
2337
|
+
}
|
|
2338
|
+
return removed;
|
|
2339
|
+
}
|
|
2340
|
+
/**
|
|
2341
|
+
* Bind a list of declarative `Hook` metadata definitions to this engine.
|
|
2342
|
+
*
|
|
2343
|
+
* Convenience proxy to the canonical `bindHooksToEngine` so callers do
|
|
2344
|
+
* not need a separate import. Use `import { bindHooksToEngine } from
|
|
2345
|
+
* '@objectstack/objectql'` directly when you want the result object.
|
|
2346
|
+
*/
|
|
2347
|
+
bindHooks(hooks, opts) {
|
|
2348
|
+
const merged = { ...opts ?? {}, logger: this.logger };
|
|
2349
|
+
if (!merged.bodyRunner && this._defaultBodyRunner) {
|
|
2350
|
+
merged.bodyRunner = this._defaultBodyRunner;
|
|
2351
|
+
}
|
|
2352
|
+
if (merged.strict === void 0 && this._strictHookBinding) {
|
|
2353
|
+
merged.strict = true;
|
|
2354
|
+
}
|
|
2355
|
+
if (merged.warnLegacyHandler === void 0 && this._warnLegacyHandler) {
|
|
2356
|
+
merged.warnLegacyHandler = true;
|
|
2357
|
+
}
|
|
2358
|
+
if (!merged.metrics && this._hookMetricsRecorder) {
|
|
2359
|
+
merged.metrics = this._hookMetricsRecorder;
|
|
2360
|
+
}
|
|
2361
|
+
bindHooksToEngine(this, hooks, merged);
|
|
2362
|
+
}
|
|
2363
|
+
/**
|
|
2364
|
+
* Install a default body-runner used when `bindHooks` is called without
|
|
2365
|
+
* an explicit one. The runtime layer sets this once on each per-project
|
|
2366
|
+
* engine so every binding path (template seed, metadata sync, AppPlugin)
|
|
2367
|
+
* can execute hook `body.source` consistently.
|
|
2368
|
+
*/
|
|
2369
|
+
setDefaultBodyRunner(runner) {
|
|
2370
|
+
this._defaultBodyRunner = runner;
|
|
2371
|
+
}
|
|
2372
|
+
/**
|
|
2373
|
+
* Toggle strict hook-binding mode for this engine. When enabled, every
|
|
2374
|
+
* subsequent `bindHooks` call rejects on the first unresolved hook
|
|
2375
|
+
* instead of silently warning. Production runtimes should enable this.
|
|
2376
|
+
*/
|
|
2377
|
+
setStrictHookBinding(strict) {
|
|
2378
|
+
this._strictHookBinding = strict;
|
|
2379
|
+
}
|
|
2380
|
+
/** Toggle deprecation warnings for hooks still using legacy `handler` ref. */
|
|
2381
|
+
setWarnLegacyHandler(warn) {
|
|
2382
|
+
this._warnLegacyHandler = warn;
|
|
2383
|
+
}
|
|
2384
|
+
/**
|
|
2385
|
+
* Install a metrics recorder used by every subsequent `bindHooks` call.
|
|
2386
|
+
* The recorder's methods are invoked per-execution to count outcomes
|
|
2387
|
+
* (success / error / timeout / capability_rejected), skips, and retries.
|
|
2388
|
+
* Defaults to no-op so the engine pays zero cost when nobody is observing.
|
|
2389
|
+
*/
|
|
2390
|
+
setHookMetricsRecorder(recorder) {
|
|
2391
|
+
this._hookMetricsRecorder = recorder;
|
|
2392
|
+
}
|
|
2393
|
+
/** Read the engine's installed metrics recorder, if any. */
|
|
2394
|
+
getHookMetricsRecorder() {
|
|
2395
|
+
return this._hookMetricsRecorder;
|
|
2396
|
+
}
|
|
1646
2397
|
async triggerHooks(event, context) {
|
|
1647
2398
|
const entries = this.hooks.get(event) || [];
|
|
1648
2399
|
if (entries.length === 0) {
|
|
@@ -1738,6 +2489,62 @@ var _ObjectQL = class _ObjectQL {
|
|
|
1738
2489
|
accessToken: execCtx.accessToken
|
|
1739
2490
|
};
|
|
1740
2491
|
}
|
|
2492
|
+
/**
|
|
2493
|
+
* Build a HookContext.api: a ScopedContext that hooks can use to
|
|
2494
|
+
* read/write other objects within the same execution context.
|
|
2495
|
+
* Falls back to a system-elevated empty context when no execCtx
|
|
2496
|
+
* is supplied (e.g. system-triggered hooks).
|
|
2497
|
+
*/
|
|
2498
|
+
buildHookApi(execCtx) {
|
|
2499
|
+
const safeCtx = execCtx ?? { isSystem: true };
|
|
2500
|
+
return new ScopedContext(safeCtx, this);
|
|
2501
|
+
}
|
|
2502
|
+
/**
|
|
2503
|
+
* Apply field defaults to an incoming insert payload. Defaults that are
|
|
2504
|
+
* Expression envelopes (e.g. `{ dialect: 'cel', source: 'today()' }`,
|
|
2505
|
+
* `{ dialect: 'cel', source: 'os.user.id' }`) are evaluated via
|
|
2506
|
+
* `ExpressionEngine` against the calling user/org/now snapshot. Static
|
|
2507
|
+
* defaults are applied verbatim. Records that already supplied a value for a
|
|
2508
|
+
* field are left untouched.
|
|
2509
|
+
*
|
|
2510
|
+
* Implements ROADMAP §M9.9b — `defaultValue` accepts Expression so authors
|
|
2511
|
+
* can replace "write a hook to default to today/current-user" with a
|
|
2512
|
+
* declarative `defaultValue: cel\`today()\``.
|
|
2513
|
+
*/
|
|
2514
|
+
applyFieldDefaults(object, record, execCtx, nowSnapshot) {
|
|
2515
|
+
const schema = this.getSchema(object);
|
|
2516
|
+
const fieldsRaw = schema?.fields;
|
|
2517
|
+
if (!fieldsRaw || typeof fieldsRaw !== "object") return record;
|
|
2518
|
+
const fieldEntries = Array.isArray(fieldsRaw) ? fieldsRaw : Object.entries(fieldsRaw).map(([name, def]) => ({ name, ...def }));
|
|
2519
|
+
const out = { ...record };
|
|
2520
|
+
const now = nowSnapshot ?? /* @__PURE__ */ new Date();
|
|
2521
|
+
for (const f of fieldEntries) {
|
|
2522
|
+
if (out[f.name] !== void 0) continue;
|
|
2523
|
+
if (f.defaultValue == null) continue;
|
|
2524
|
+
const dv = f.defaultValue;
|
|
2525
|
+
if (typeof dv === "object" && dv !== null && dv.dialect && typeof dv.source === "string") {
|
|
2526
|
+
const result = import_formula2.ExpressionEngine.evaluate(dv, {
|
|
2527
|
+
now,
|
|
2528
|
+
user: execCtx?.userId ? { id: String(execCtx.userId), role: execCtx?.roles?.[0] } : void 0,
|
|
2529
|
+
org: execCtx?.tenantId ? { id: String(execCtx.tenantId) } : void 0,
|
|
2530
|
+
record: out,
|
|
2531
|
+
extra: { object }
|
|
2532
|
+
});
|
|
2533
|
+
if (result.ok) {
|
|
2534
|
+
out[f.name] = result.value;
|
|
2535
|
+
} else {
|
|
2536
|
+
this.logger.warn("Failed to evaluate default expression", {
|
|
2537
|
+
object,
|
|
2538
|
+
field: f.name,
|
|
2539
|
+
error: result.error
|
|
2540
|
+
});
|
|
2541
|
+
}
|
|
2542
|
+
} else {
|
|
2543
|
+
out[f.name] = dv;
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
return out;
|
|
2547
|
+
}
|
|
1741
2548
|
/**
|
|
1742
2549
|
* Register contribution (Manifest)
|
|
1743
2550
|
*
|
|
@@ -1752,20 +2559,24 @@ var _ObjectQL = class _ObjectQL {
|
|
|
1752
2559
|
const id = manifest.id || manifest.name;
|
|
1753
2560
|
const namespace = manifest.namespace;
|
|
1754
2561
|
this.logger.debug("Registering package manifest", { id, namespace });
|
|
1755
|
-
|
|
2562
|
+
console.warn(`[ObjectQL:registerApp] id=${id} flows=${Array.isArray(manifest.flows) ? manifest.flows.length : typeof manifest.flows} keys=${Object.keys(manifest).join(",")}`);
|
|
2563
|
+
if (id) {
|
|
2564
|
+
this.manifests.set(id, manifest);
|
|
2565
|
+
}
|
|
2566
|
+
this._registry.installPackage(manifest);
|
|
1756
2567
|
this.logger.debug("Installed Package", { id: manifest.id, name: manifest.name, namespace });
|
|
1757
2568
|
if (manifest.objects) {
|
|
1758
2569
|
if (Array.isArray(manifest.objects)) {
|
|
1759
2570
|
this.logger.debug("Registering objects from manifest (Array)", { id, objectCount: manifest.objects.length });
|
|
1760
2571
|
for (const objDef of manifest.objects) {
|
|
1761
|
-
const fqn =
|
|
2572
|
+
const fqn = this._registry.registerObject(objDef, id, namespace, "own");
|
|
1762
2573
|
this.logger.debug("Registered Object", { fqn, from: id });
|
|
1763
2574
|
}
|
|
1764
2575
|
} else {
|
|
1765
2576
|
this.logger.debug("Registering objects from manifest (Map)", { id, objectCount: Object.keys(manifest.objects).length });
|
|
1766
2577
|
for (const [name, objDef] of Object.entries(manifest.objects)) {
|
|
1767
2578
|
objDef.name = name;
|
|
1768
|
-
const fqn =
|
|
2579
|
+
const fqn = this._registry.registerObject(objDef, id, namespace, "own");
|
|
1769
2580
|
this.logger.debug("Registered Object", { fqn, from: id });
|
|
1770
2581
|
}
|
|
1771
2582
|
}
|
|
@@ -1785,7 +2596,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
1785
2596
|
validations: ext.validations,
|
|
1786
2597
|
indexes: ext.indexes
|
|
1787
2598
|
};
|
|
1788
|
-
|
|
2599
|
+
this._registry.registerObject(extDef, id, void 0, "extend", priority);
|
|
1789
2600
|
this.logger.debug("Registered Object Extension", { target: targetFqn, priority, from: id });
|
|
1790
2601
|
}
|
|
1791
2602
|
}
|
|
@@ -1794,13 +2605,15 @@ var _ObjectQL = class _ObjectQL {
|
|
|
1794
2605
|
for (const app of manifest.apps) {
|
|
1795
2606
|
const appName = app.name || app.id;
|
|
1796
2607
|
if (appName) {
|
|
1797
|
-
|
|
2608
|
+
const resolved = namespace ? this.resolveNavObjectNames(app, namespace) : app;
|
|
2609
|
+
this._registry.registerApp(resolved, id);
|
|
1798
2610
|
this.logger.debug("Registered App", { app: appName, from: id });
|
|
1799
2611
|
}
|
|
1800
2612
|
}
|
|
1801
2613
|
}
|
|
1802
2614
|
if (manifest.name && manifest.navigation && !manifest.apps?.length) {
|
|
1803
|
-
|
|
2615
|
+
const resolved = namespace ? this.resolveNavObjectNames(manifest, namespace) : manifest;
|
|
2616
|
+
this._registry.registerApp(resolved, id);
|
|
1804
2617
|
this.logger.debug("Registered manifest-as-app", { app: manifest.name, from: id });
|
|
1805
2618
|
}
|
|
1806
2619
|
const metadataArrayKeys = [
|
|
@@ -1839,9 +2652,12 @@ var _ObjectQL = class _ObjectQL {
|
|
|
1839
2652
|
if (Array.isArray(items) && items.length > 0) {
|
|
1840
2653
|
this.logger.debug(`Registering ${key} from manifest`, { id, count: items.length });
|
|
1841
2654
|
for (const item of items) {
|
|
1842
|
-
const itemName =
|
|
2655
|
+
const itemName = resolveMetadataItemName(key, item);
|
|
1843
2656
|
if (itemName) {
|
|
1844
|
-
|
|
2657
|
+
const toRegister = item.name === itemName ? item : { ...item, name: itemName };
|
|
2658
|
+
this._registry.registerItem((0, import_shared2.pluralToSingular)(key), toRegister, "name", id);
|
|
2659
|
+
} else {
|
|
2660
|
+
this.logger.warn(`Skipping ${(0, import_shared2.pluralToSingular)(key)} without a derivable name`, { id });
|
|
1845
2661
|
}
|
|
1846
2662
|
}
|
|
1847
2663
|
}
|
|
@@ -1851,14 +2667,14 @@ var _ObjectQL = class _ObjectQL {
|
|
|
1851
2667
|
this.logger.debug("Registering seed data datasets", { id, count: seedData.length });
|
|
1852
2668
|
for (const dataset of seedData) {
|
|
1853
2669
|
if (dataset.object) {
|
|
1854
|
-
|
|
2670
|
+
this._registry.registerItem("data", dataset, "object", id);
|
|
1855
2671
|
}
|
|
1856
2672
|
}
|
|
1857
2673
|
}
|
|
1858
2674
|
if (manifest.contributes?.kinds) {
|
|
1859
2675
|
this.logger.debug("Registering kinds from manifest", { id, kindCount: manifest.contributes.kinds.length });
|
|
1860
2676
|
for (const kind of manifest.contributes.kinds) {
|
|
1861
|
-
|
|
2677
|
+
this._registry.registerKind(kind);
|
|
1862
2678
|
this.logger.debug("Registered Kind", { kind: kind.name || kind.type, from: id });
|
|
1863
2679
|
}
|
|
1864
2680
|
}
|
|
@@ -1873,6 +2689,25 @@ var _ObjectQL = class _ObjectQL {
|
|
|
1873
2689
|
}
|
|
1874
2690
|
}
|
|
1875
2691
|
}
|
|
2692
|
+
/**
|
|
2693
|
+
* Deep-clone an app definition, resolving objectName references in navigation
|
|
2694
|
+
* items via the registry. Object names are canonical identifiers — no FQN
|
|
2695
|
+
* expansion is applied.
|
|
2696
|
+
*/
|
|
2697
|
+
resolveNavObjectNames(app, namespace) {
|
|
2698
|
+
if (!app.navigation) return app;
|
|
2699
|
+
const resolveItems = (items) => items.map((item) => {
|
|
2700
|
+
const resolved = { ...item };
|
|
2701
|
+
if (resolved.objectName && !resolved.objectName.includes("__")) {
|
|
2702
|
+
resolved.objectName = computeFQN(namespace, resolved.objectName);
|
|
2703
|
+
}
|
|
2704
|
+
if (Array.isArray(resolved.children)) {
|
|
2705
|
+
resolved.children = resolveItems(resolved.children);
|
|
2706
|
+
}
|
|
2707
|
+
return resolved;
|
|
2708
|
+
});
|
|
2709
|
+
return { ...app, navigation: resolveItems(app.navigation) };
|
|
2710
|
+
}
|
|
1876
2711
|
/**
|
|
1877
2712
|
* Register a nested plugin's metadata (objects, actions, views, etc.)
|
|
1878
2713
|
*
|
|
@@ -1893,7 +2728,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
1893
2728
|
if (Array.isArray(plugin.objects)) {
|
|
1894
2729
|
this.logger.debug("Registering plugin objects (Array)", { pluginName, count: plugin.objects.length });
|
|
1895
2730
|
for (const objDef of plugin.objects) {
|
|
1896
|
-
const fqn =
|
|
2731
|
+
const fqn = this._registry.registerObject(objDef, ownerId, pluginNamespace, "own");
|
|
1897
2732
|
this.logger.debug("Registered Object", { fqn, from: pluginName });
|
|
1898
2733
|
}
|
|
1899
2734
|
} else {
|
|
@@ -1901,7 +2736,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
1901
2736
|
this.logger.debug("Registering plugin objects (Map)", { pluginName, count: entries.length });
|
|
1902
2737
|
for (const [name, objDef] of entries) {
|
|
1903
2738
|
objDef.name = name;
|
|
1904
|
-
const fqn =
|
|
2739
|
+
const fqn = this._registry.registerObject(objDef, ownerId, pluginNamespace, "own");
|
|
1905
2740
|
this.logger.debug("Registered Object", { fqn, from: pluginName });
|
|
1906
2741
|
}
|
|
1907
2742
|
}
|
|
@@ -1911,7 +2746,8 @@ var _ObjectQL = class _ObjectQL {
|
|
|
1911
2746
|
}
|
|
1912
2747
|
if (plugin.name && plugin.navigation) {
|
|
1913
2748
|
try {
|
|
1914
|
-
|
|
2749
|
+
const resolved = pluginNamespace ? this.resolveNavObjectNames(plugin, pluginNamespace) : plugin;
|
|
2750
|
+
this._registry.registerApp(resolved, ownerId);
|
|
1915
2751
|
this.logger.debug("Registered plugin-as-app", { app: plugin.name, from: pluginName });
|
|
1916
2752
|
} catch (err) {
|
|
1917
2753
|
this.logger.warn("Failed to register plugin as app", { pluginName, error: err.message });
|
|
@@ -1945,9 +2781,10 @@ var _ObjectQL = class _ObjectQL {
|
|
|
1945
2781
|
const items = plugin[key];
|
|
1946
2782
|
if (Array.isArray(items) && items.length > 0) {
|
|
1947
2783
|
for (const item of items) {
|
|
1948
|
-
const itemName =
|
|
2784
|
+
const itemName = resolveMetadataItemName(key, item);
|
|
1949
2785
|
if (itemName) {
|
|
1950
|
-
|
|
2786
|
+
const toRegister = item.name === itemName ? item : { ...item, name: itemName };
|
|
2787
|
+
this._registry.registerItem((0, import_shared2.pluralToSingular)(key), toRegister, "name", ownerId);
|
|
1951
2788
|
}
|
|
1952
2789
|
}
|
|
1953
2790
|
}
|
|
@@ -1985,48 +2822,117 @@ var _ObjectQL = class _ObjectQL {
|
|
|
1985
2822
|
* Helper to get object definition
|
|
1986
2823
|
*/
|
|
1987
2824
|
getSchema(objectName) {
|
|
1988
|
-
return
|
|
2825
|
+
return this._registry.getObject(objectName);
|
|
1989
2826
|
}
|
|
1990
2827
|
/**
|
|
1991
|
-
* Resolve
|
|
1992
|
-
*
|
|
1993
|
-
*
|
|
1994
|
-
*
|
|
1995
|
-
*
|
|
1996
|
-
*
|
|
1997
|
-
* This ensures that all driver operations use a consistent key
|
|
1998
|
-
* regardless of whether the caller uses the short name or FQN.
|
|
2828
|
+
* Resolve any object identifier to the physical storage name used by drivers.
|
|
2829
|
+
*
|
|
2830
|
+
* Accepts the canonical short name (e.g., 'account') or, for explicit
|
|
2831
|
+
* cross-package disambiguation, the canonical object name (e.g., 'account'). The result is
|
|
2832
|
+
* the physical table name derived via `StorageNameMapping.resolveTableName`.
|
|
1999
2833
|
*/
|
|
2000
2834
|
resolveObjectName(name) {
|
|
2001
|
-
const schema =
|
|
2835
|
+
const schema = this._registry.getObject(name);
|
|
2002
2836
|
if (schema) {
|
|
2003
|
-
return
|
|
2837
|
+
return import_system.StorageNameMapping.resolveTableName(schema);
|
|
2004
2838
|
}
|
|
2005
|
-
return name;
|
|
2839
|
+
return import_system.StorageNameMapping.resolveTableName({ name });
|
|
2006
2840
|
}
|
|
2007
2841
|
/**
|
|
2008
2842
|
* Helper to get the target driver
|
|
2843
|
+
*
|
|
2844
|
+
* Resolution priority (first match wins):
|
|
2845
|
+
* 1. Object's explicit `datasource` field (if not 'default')
|
|
2846
|
+
* 2. DatasourceMapping rules (namespace/package/pattern matching)
|
|
2847
|
+
* 3. Package's `defaultDatasource` from manifest
|
|
2848
|
+
* 4. Global default driver
|
|
2009
2849
|
*/
|
|
2010
2850
|
getDriver(objectName) {
|
|
2011
|
-
const object =
|
|
2012
|
-
if (object) {
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2851
|
+
const object = this._registry.getObject(objectName);
|
|
2852
|
+
if (object?.datasource && object.datasource !== "default") {
|
|
2853
|
+
if (this.drivers.has(object.datasource)) {
|
|
2854
|
+
return this.drivers.get(object.datasource);
|
|
2855
|
+
}
|
|
2856
|
+
throw new Error(`[ObjectQL] Datasource '${object.datasource}' configured for object '${objectName}' is not registered.`);
|
|
2857
|
+
}
|
|
2858
|
+
const mappedDatasource = this.resolveDatasourceFromMapping(objectName, object);
|
|
2859
|
+
if (mappedDatasource && this.drivers.has(mappedDatasource)) {
|
|
2860
|
+
this.logger.debug("Resolved datasource from mapping", {
|
|
2861
|
+
object: objectName,
|
|
2862
|
+
datasource: mappedDatasource
|
|
2863
|
+
});
|
|
2864
|
+
return this.drivers.get(mappedDatasource);
|
|
2865
|
+
}
|
|
2866
|
+
const fqn = object?.name || objectName;
|
|
2867
|
+
const owner = this._registry.getObjectOwner(fqn);
|
|
2868
|
+
if (owner?.packageId) {
|
|
2869
|
+
const manifest = this.manifests.get(owner.packageId);
|
|
2870
|
+
if (manifest?.defaultDatasource && manifest.defaultDatasource !== "default") {
|
|
2871
|
+
if (this.drivers.has(manifest.defaultDatasource)) {
|
|
2872
|
+
this.logger.debug("Resolved datasource from package manifest", {
|
|
2873
|
+
object: objectName,
|
|
2874
|
+
package: owner.packageId,
|
|
2875
|
+
datasource: manifest.defaultDatasource
|
|
2876
|
+
});
|
|
2877
|
+
return this.drivers.get(manifest.defaultDatasource);
|
|
2021
2878
|
}
|
|
2022
|
-
throw new Error(`[ObjectQL] Datasource '${datasourceName}' configured for object '${objectName}' is not registered.`);
|
|
2023
2879
|
}
|
|
2024
2880
|
}
|
|
2025
|
-
if (this.defaultDriver) {
|
|
2881
|
+
if (this.defaultDriver && this.drivers.has(this.defaultDriver)) {
|
|
2026
2882
|
return this.drivers.get(this.defaultDriver);
|
|
2027
2883
|
}
|
|
2028
2884
|
throw new Error(`[ObjectQL] No driver available for object '${objectName}'`);
|
|
2029
2885
|
}
|
|
2886
|
+
/**
|
|
2887
|
+
* Resolve datasource from mapping rules
|
|
2888
|
+
*
|
|
2889
|
+
* Rules are evaluated in order (or by priority if specified).
|
|
2890
|
+
* First matching rule wins.
|
|
2891
|
+
*/
|
|
2892
|
+
resolveDatasourceFromMapping(objectName, object) {
|
|
2893
|
+
if (!this.datasourceMapping || this.datasourceMapping.length === 0) {
|
|
2894
|
+
return null;
|
|
2895
|
+
}
|
|
2896
|
+
const sortedRules = [...this.datasourceMapping].sort((a, b) => {
|
|
2897
|
+
const aPriority = a.priority ?? 1e3;
|
|
2898
|
+
const bPriority = b.priority ?? 1e3;
|
|
2899
|
+
return aPriority - bPriority;
|
|
2900
|
+
});
|
|
2901
|
+
for (const rule of sortedRules) {
|
|
2902
|
+
if (rule.namespace && object?.namespace === rule.namespace) {
|
|
2903
|
+
return rule.datasource;
|
|
2904
|
+
}
|
|
2905
|
+
if (rule.package && object?.packageId === rule.package) {
|
|
2906
|
+
return rule.datasource;
|
|
2907
|
+
}
|
|
2908
|
+
if (rule.objectPattern && this.matchPattern(objectName, rule.objectPattern)) {
|
|
2909
|
+
return rule.datasource;
|
|
2910
|
+
}
|
|
2911
|
+
if (rule.default) {
|
|
2912
|
+
return rule.datasource;
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
return null;
|
|
2916
|
+
}
|
|
2917
|
+
/**
|
|
2918
|
+
* Simple glob pattern matching
|
|
2919
|
+
* Supports * (any chars) and ? (single char)
|
|
2920
|
+
*/
|
|
2921
|
+
matchPattern(objectName, pattern) {
|
|
2922
|
+
const regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
2923
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
2924
|
+
return regex.test(objectName);
|
|
2925
|
+
}
|
|
2926
|
+
/**
|
|
2927
|
+
* Set datasource mapping rules
|
|
2928
|
+
* Called by ObjectQLPlugin during bootstrap
|
|
2929
|
+
*/
|
|
2930
|
+
setDatasourceMapping(rules) {
|
|
2931
|
+
this.datasourceMapping = rules;
|
|
2932
|
+
this.logger.info("Datasource mapping rules configured", {
|
|
2933
|
+
ruleCount: rules.length
|
|
2934
|
+
});
|
|
2935
|
+
}
|
|
2030
2936
|
/**
|
|
2031
2937
|
* Initialize the engine and all registered drivers
|
|
2032
2938
|
*/
|
|
@@ -2079,7 +2985,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
2079
2985
|
async expandRelatedRecords(objectName, records, expand, depth = 0) {
|
|
2080
2986
|
if (!records || records.length === 0) return records;
|
|
2081
2987
|
if (depth >= _ObjectQL.MAX_EXPAND_DEPTH) return records;
|
|
2082
|
-
const objectSchema =
|
|
2988
|
+
const objectSchema = this._registry.getObject(objectName);
|
|
2083
2989
|
if (!objectSchema || !objectSchema.fields) return records;
|
|
2084
2990
|
for (const [fieldName, nestedAST] of Object.entries(expand)) {
|
|
2085
2991
|
const fieldDef = objectSchema.fields[fieldName];
|
|
@@ -2160,6 +3066,20 @@ var _ObjectQL = class _ObjectQL {
|
|
|
2160
3066
|
ast.limit = ast.top;
|
|
2161
3067
|
}
|
|
2162
3068
|
delete ast.top;
|
|
3069
|
+
const _findSchema = this._registry.getObject(object);
|
|
3070
|
+
const _findFormula = planFormulaProjection(_findSchema, ast.fields);
|
|
3071
|
+
if (_findFormula.projected) ast.fields = _findFormula.projected;
|
|
3072
|
+
if (_findSchema?.fields && Array.isArray(ast.fields) && ast.fields.length > 0) {
|
|
3073
|
+
const known = new Set(Object.keys(_findSchema.fields));
|
|
3074
|
+
known.add("id");
|
|
3075
|
+
known.add("created_at");
|
|
3076
|
+
known.add("updated_at");
|
|
3077
|
+
const filtered = ast.fields.filter((f) => {
|
|
3078
|
+
const head = String(f).split(".")[0];
|
|
3079
|
+
return known.has(head);
|
|
3080
|
+
});
|
|
3081
|
+
ast.fields = filtered.length > 0 ? filtered : void 0;
|
|
3082
|
+
}
|
|
2163
3083
|
const opCtx = {
|
|
2164
3084
|
object,
|
|
2165
3085
|
operation: "find",
|
|
@@ -2173,12 +3093,14 @@ var _ObjectQL = class _ObjectQL {
|
|
|
2173
3093
|
event: "beforeFind",
|
|
2174
3094
|
input: { ast: opCtx.ast, options: opCtx.options },
|
|
2175
3095
|
session: this.buildSession(opCtx.context),
|
|
3096
|
+
api: this.buildHookApi(opCtx.context),
|
|
2176
3097
|
transaction: opCtx.context?.transaction,
|
|
2177
3098
|
ql: this
|
|
2178
3099
|
};
|
|
2179
3100
|
await this.triggerHooks("beforeFind", hookContext);
|
|
2180
3101
|
try {
|
|
2181
3102
|
let result = await driver.find(object, hookContext.input.ast, hookContext.input.options);
|
|
3103
|
+
if (Array.isArray(result)) applyFormulaPlan(_findFormula.plan, result);
|
|
2182
3104
|
if (ast.expand && Object.keys(ast.expand).length > 0 && Array.isArray(result)) {
|
|
2183
3105
|
result = await this.expandRelatedRecords(object, result, ast.expand, 0);
|
|
2184
3106
|
}
|
|
@@ -2200,6 +3122,17 @@ var _ObjectQL = class _ObjectQL {
|
|
|
2200
3122
|
const ast = { object: objectName, ...query, limit: 1 };
|
|
2201
3123
|
delete ast.context;
|
|
2202
3124
|
delete ast.top;
|
|
3125
|
+
const _findOneSchema = this._registry.getObject(objectName);
|
|
3126
|
+
const _findOneFormula = planFormulaProjection(_findOneSchema, ast.fields);
|
|
3127
|
+
if (_findOneFormula.projected) ast.fields = _findOneFormula.projected;
|
|
3128
|
+
if (_findOneSchema?.fields && Array.isArray(ast.fields) && ast.fields.length > 0) {
|
|
3129
|
+
const known = new Set(Object.keys(_findOneSchema.fields));
|
|
3130
|
+
known.add("id");
|
|
3131
|
+
known.add("created_at");
|
|
3132
|
+
known.add("updated_at");
|
|
3133
|
+
const filtered = ast.fields.filter((f) => known.has(String(f).split(".")[0]));
|
|
3134
|
+
ast.fields = filtered.length > 0 ? filtered : void 0;
|
|
3135
|
+
}
|
|
2203
3136
|
const opCtx = {
|
|
2204
3137
|
object: objectName,
|
|
2205
3138
|
operation: "findOne",
|
|
@@ -2209,6 +3142,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
2209
3142
|
};
|
|
2210
3143
|
await this.executeWithMiddleware(opCtx, async () => {
|
|
2211
3144
|
let result = await driver.findOne(objectName, opCtx.ast);
|
|
3145
|
+
if (result != null) applyFormulaPlan(_findOneFormula.plan, [result]);
|
|
2212
3146
|
if (ast.expand && Object.keys(ast.expand).length > 0 && result != null) {
|
|
2213
3147
|
const expanded = await this.expandRelatedRecords(objectName, [result], ast.expand, 0);
|
|
2214
3148
|
result = expanded[0];
|
|
@@ -2234,20 +3168,31 @@ var _ObjectQL = class _ObjectQL {
|
|
|
2234
3168
|
event: "beforeInsert",
|
|
2235
3169
|
input: { data: opCtx.data, options: opCtx.options },
|
|
2236
3170
|
session: this.buildSession(opCtx.context),
|
|
3171
|
+
api: this.buildHookApi(opCtx.context),
|
|
2237
3172
|
transaction: opCtx.context?.transaction,
|
|
2238
3173
|
ql: this
|
|
2239
3174
|
};
|
|
2240
3175
|
await this.triggerHooks("beforeInsert", hookContext);
|
|
2241
3176
|
try {
|
|
2242
3177
|
let result;
|
|
3178
|
+
const nowSnap = /* @__PURE__ */ new Date();
|
|
2243
3179
|
if (Array.isArray(hookContext.input.data)) {
|
|
3180
|
+
const rows = hookContext.input.data.map(
|
|
3181
|
+
(row) => this.applyFieldDefaults(object, row, opCtx.context, nowSnap)
|
|
3182
|
+
);
|
|
2244
3183
|
if (driver.bulkCreate) {
|
|
2245
|
-
result = await driver.bulkCreate(object,
|
|
3184
|
+
result = await driver.bulkCreate(object, rows, hookContext.input.options);
|
|
2246
3185
|
} else {
|
|
2247
|
-
result = await Promise.all(
|
|
3186
|
+
result = await Promise.all(rows.map((item) => driver.create(object, item, hookContext.input.options)));
|
|
2248
3187
|
}
|
|
2249
3188
|
} else {
|
|
2250
|
-
|
|
3189
|
+
const row = this.applyFieldDefaults(
|
|
3190
|
+
object,
|
|
3191
|
+
hookContext.input.data,
|
|
3192
|
+
opCtx.context,
|
|
3193
|
+
nowSnap
|
|
3194
|
+
);
|
|
3195
|
+
result = await driver.create(object, row, hookContext.input.options);
|
|
2251
3196
|
}
|
|
2252
3197
|
hookContext.event = "afterInsert";
|
|
2253
3198
|
hookContext.result = result;
|
|
@@ -2314,6 +3259,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
2314
3259
|
event: "beforeUpdate",
|
|
2315
3260
|
input: { id, data: opCtx.data, options: opCtx.options },
|
|
2316
3261
|
session: this.buildSession(opCtx.context),
|
|
3262
|
+
api: this.buildHookApi(opCtx.context),
|
|
2317
3263
|
transaction: opCtx.context?.transaction,
|
|
2318
3264
|
ql: this
|
|
2319
3265
|
};
|
|
@@ -2379,6 +3325,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
2379
3325
|
event: "beforeDelete",
|
|
2380
3326
|
input: { id, options: opCtx.options },
|
|
2381
3327
|
session: this.buildSession(opCtx.context),
|
|
3328
|
+
api: this.buildHookApi(opCtx.context),
|
|
2382
3329
|
transaction: opCtx.context?.transaction,
|
|
2383
3330
|
ql: this
|
|
2384
3331
|
};
|
|
@@ -2458,18 +3405,42 @@ var _ObjectQL = class _ObjectQL {
|
|
|
2458
3405
|
groupBy: query.groupBy,
|
|
2459
3406
|
aggregations: query.aggregations
|
|
2460
3407
|
};
|
|
3408
|
+
const drv = driver;
|
|
3409
|
+
if (typeof drv.aggregate === "function") {
|
|
3410
|
+
return drv.aggregate(object, ast);
|
|
3411
|
+
}
|
|
2461
3412
|
return driver.find(object, ast);
|
|
2462
3413
|
});
|
|
2463
3414
|
return opCtx.result;
|
|
2464
3415
|
}
|
|
2465
3416
|
async execute(command, options) {
|
|
3417
|
+
let driver;
|
|
2466
3418
|
if (options?.object) {
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
3419
|
+
driver = this.getDriver(options.object);
|
|
3420
|
+
} else if (options?.datasource && this.drivers.has(options.datasource)) {
|
|
3421
|
+
driver = this.drivers.get(options.datasource);
|
|
3422
|
+
} else if (this.defaultDriver && this.drivers.has(this.defaultDriver)) {
|
|
3423
|
+
driver = this.drivers.get(this.defaultDriver);
|
|
3424
|
+
} else if (this.drivers.size === 1) {
|
|
3425
|
+
driver = this.drivers.values().next().value;
|
|
3426
|
+
}
|
|
3427
|
+
if (!driver) {
|
|
3428
|
+
throw new Error(
|
|
3429
|
+
"Execute requires options.object to select a driver, or a default driver to be configured. Configure datasourceMapping with `default: true` or pass `{ object }` / `{ datasource }` in options."
|
|
3430
|
+
);
|
|
3431
|
+
}
|
|
3432
|
+
if (!driver.execute) {
|
|
3433
|
+
throw new Error("Selected driver does not implement execute()");
|
|
3434
|
+
}
|
|
3435
|
+
let rawCommand = command;
|
|
3436
|
+
let params = options?.args ?? options?.params;
|
|
3437
|
+
if (command && typeof command === "object" && !Array.isArray(command) && "sql" in command) {
|
|
3438
|
+
rawCommand = command.sql;
|
|
3439
|
+
if (params === void 0) {
|
|
3440
|
+
params = command.args ?? command.params;
|
|
2470
3441
|
}
|
|
2471
3442
|
}
|
|
2472
|
-
|
|
3443
|
+
return driver.execute(rawCommand, params, options);
|
|
2473
3444
|
}
|
|
2474
3445
|
// ============================================
|
|
2475
3446
|
// Compatibility / Convenience API
|
|
@@ -2490,16 +3461,16 @@ var _ObjectQL = class _ObjectQL {
|
|
|
2490
3461
|
}
|
|
2491
3462
|
}
|
|
2492
3463
|
}
|
|
2493
|
-
return
|
|
3464
|
+
return this._registry.registerObject(schema, packageId, namespace);
|
|
2494
3465
|
}
|
|
2495
3466
|
/**
|
|
2496
3467
|
* Unregister a single object by name.
|
|
2497
3468
|
*/
|
|
2498
3469
|
unregisterObject(name, packageId) {
|
|
2499
3470
|
if (packageId) {
|
|
2500
|
-
|
|
3471
|
+
this._registry.unregisterObjectsByPackage(packageId);
|
|
2501
3472
|
} else {
|
|
2502
|
-
|
|
3473
|
+
this._registry.unregisterItem("object", name);
|
|
2503
3474
|
}
|
|
2504
3475
|
}
|
|
2505
3476
|
/**
|
|
@@ -2515,7 +3486,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
2515
3486
|
*/
|
|
2516
3487
|
getConfigs() {
|
|
2517
3488
|
const result = {};
|
|
2518
|
-
const objects =
|
|
3489
|
+
const objects = this._registry.getAllObjects();
|
|
2519
3490
|
for (const obj of objects) {
|
|
2520
3491
|
if (obj.name) {
|
|
2521
3492
|
result[obj.name] = obj;
|
|
@@ -2549,10 +3520,32 @@ var _ObjectQL = class _ObjectQL {
|
|
|
2549
3520
|
return void 0;
|
|
2550
3521
|
}
|
|
2551
3522
|
}
|
|
3523
|
+
/**
|
|
3524
|
+
* Sync all registered object schemas to their respective drivers.
|
|
3525
|
+
* Call this after dynamically registering new objects at runtime
|
|
3526
|
+
* (e.g. after template seeding) to ensure tables/collections exist
|
|
3527
|
+
* before inserting seed data.
|
|
3528
|
+
*/
|
|
3529
|
+
async syncSchemas() {
|
|
3530
|
+
const allObjects = this._registry.getAllObjects();
|
|
3531
|
+
for (const obj of allObjects) {
|
|
3532
|
+
const driver = this.getDriverForObject(obj.name);
|
|
3533
|
+
if (!driver) continue;
|
|
3534
|
+
const tableName = import_system.StorageNameMapping.resolveTableName(obj);
|
|
3535
|
+
if (typeof driver.syncSchemasBatch === "function" && driver.supports?.batchSchemaSync) {
|
|
3536
|
+
}
|
|
3537
|
+
if (typeof driver.syncSchema === "function") {
|
|
3538
|
+
try {
|
|
3539
|
+
await driver.syncSchema(tableName, obj);
|
|
3540
|
+
} catch {
|
|
3541
|
+
}
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
2552
3545
|
/**
|
|
2553
3546
|
* Get a registered driver by datasource name.
|
|
2554
3547
|
* Alias matching @objectql/core datasource() API.
|
|
2555
|
-
*
|
|
3548
|
+
*
|
|
2556
3549
|
* @throws Error if the datasource is not found
|
|
2557
3550
|
*/
|
|
2558
3551
|
datasource(name) {
|
|
@@ -2583,7 +3576,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
2583
3576
|
}
|
|
2584
3577
|
}
|
|
2585
3578
|
this.removeActionsByPackage(packageId);
|
|
2586
|
-
|
|
3579
|
+
this._registry.unregisterObjectsByPackage(packageId, true);
|
|
2587
3580
|
}
|
|
2588
3581
|
/**
|
|
2589
3582
|
* Gracefully shut down the engine, disconnecting all drivers.
|
|
@@ -2799,83 +3792,87 @@ var ScopedContext = class _ScopedContext {
|
|
|
2799
3792
|
|
|
2800
3793
|
// src/metadata-facade.ts
|
|
2801
3794
|
var MetadataFacade = class {
|
|
3795
|
+
constructor(registry) {
|
|
3796
|
+
this.registry = registry;
|
|
3797
|
+
}
|
|
2802
3798
|
/**
|
|
2803
3799
|
* Register a metadata item
|
|
2804
3800
|
*/
|
|
2805
3801
|
async register(type, name, data) {
|
|
2806
3802
|
const definition = typeof data === "object" && data !== null ? { ...data, name: data.name ?? name } : data;
|
|
2807
3803
|
if (type === "object") {
|
|
2808
|
-
|
|
3804
|
+
this.registry.registerItem(type, definition, "name");
|
|
2809
3805
|
} else {
|
|
2810
|
-
|
|
3806
|
+
this.registry.registerItem(type, definition, definition.id ? "id" : "name");
|
|
2811
3807
|
}
|
|
2812
3808
|
}
|
|
2813
3809
|
/**
|
|
2814
3810
|
* Get a metadata item by type and name
|
|
2815
3811
|
*/
|
|
2816
3812
|
async get(type, name) {
|
|
2817
|
-
const item =
|
|
3813
|
+
const item = this.registry.getItem(type, name);
|
|
2818
3814
|
return item?.content ?? item;
|
|
2819
3815
|
}
|
|
2820
3816
|
/**
|
|
2821
3817
|
* Get the raw entry (with metadata wrapper)
|
|
2822
3818
|
*/
|
|
2823
3819
|
getEntry(type, name) {
|
|
2824
|
-
return
|
|
3820
|
+
return this.registry.getItem(type, name);
|
|
2825
3821
|
}
|
|
2826
3822
|
/**
|
|
2827
3823
|
* List all items of a type
|
|
2828
3824
|
*/
|
|
2829
3825
|
async list(type) {
|
|
2830
|
-
const items =
|
|
3826
|
+
const items = this.registry.listItems(type);
|
|
2831
3827
|
return items.map((item) => item?.content ?? item);
|
|
2832
3828
|
}
|
|
2833
3829
|
/**
|
|
2834
3830
|
* Unregister a metadata item
|
|
2835
3831
|
*/
|
|
2836
3832
|
async unregister(type, name) {
|
|
2837
|
-
|
|
3833
|
+
this.registry.unregisterItem(type, name);
|
|
2838
3834
|
}
|
|
2839
3835
|
/**
|
|
2840
3836
|
* Check if a metadata item exists
|
|
2841
3837
|
*/
|
|
2842
3838
|
async exists(type, name) {
|
|
2843
|
-
const item =
|
|
3839
|
+
const item = this.registry.getItem(type, name);
|
|
2844
3840
|
return item !== void 0 && item !== null;
|
|
2845
3841
|
}
|
|
2846
3842
|
/**
|
|
2847
3843
|
* List all names of metadata items of a given type
|
|
2848
3844
|
*/
|
|
2849
3845
|
async listNames(type) {
|
|
2850
|
-
const items =
|
|
3846
|
+
const items = this.registry.listItems(type);
|
|
2851
3847
|
return items.map((item) => item?.name ?? item?.content?.name ?? "").filter(Boolean);
|
|
2852
3848
|
}
|
|
2853
3849
|
/**
|
|
2854
3850
|
* Unregister all metadata from a package
|
|
2855
3851
|
*/
|
|
2856
3852
|
async unregisterPackage(packageName) {
|
|
2857
|
-
|
|
3853
|
+
this.registry.unregisterObjectsByPackage(packageName);
|
|
2858
3854
|
}
|
|
2859
3855
|
/**
|
|
2860
3856
|
* Convenience: get object definition
|
|
2861
3857
|
*/
|
|
2862
3858
|
async getObject(name) {
|
|
2863
|
-
return
|
|
3859
|
+
return this.registry.getObject(name);
|
|
2864
3860
|
}
|
|
2865
3861
|
/**
|
|
2866
3862
|
* Convenience: list all objects
|
|
2867
3863
|
*/
|
|
2868
3864
|
async listObjects() {
|
|
2869
|
-
return
|
|
3865
|
+
return this.registry.getAllObjects();
|
|
2870
3866
|
}
|
|
2871
3867
|
};
|
|
2872
3868
|
|
|
2873
3869
|
// src/plugin.ts
|
|
3870
|
+
var import_system2 = require("@objectstack/spec/system");
|
|
2874
3871
|
function hasLoadMetaFromDb(service) {
|
|
2875
3872
|
return typeof service === "object" && service !== null && typeof service["loadMetaFromDb"] === "function";
|
|
2876
3873
|
}
|
|
2877
3874
|
var ObjectQLPlugin = class {
|
|
2878
|
-
constructor(
|
|
3875
|
+
constructor(qlOrOptions, hostContext) {
|
|
2879
3876
|
this.name = "com.objectstack.engine.objectql";
|
|
2880
3877
|
this.type = "objectql";
|
|
2881
3878
|
this.version = "1.0.0";
|
|
@@ -2900,7 +3897,9 @@ var ObjectQLPlugin = class {
|
|
|
2900
3897
|
});
|
|
2901
3898
|
const protocolShim = new ObjectStackProtocolImplementation(
|
|
2902
3899
|
this.ql,
|
|
2903
|
-
() => ctx.getServices ? ctx.getServices() : /* @__PURE__ */ new Map()
|
|
3900
|
+
() => ctx.getServices ? ctx.getServices() : /* @__PURE__ */ new Map(),
|
|
3901
|
+
void 0,
|
|
3902
|
+
this.projectId
|
|
2904
3903
|
);
|
|
2905
3904
|
ctx.registerService("protocol", protocolShim);
|
|
2906
3905
|
ctx.logger.info("Protocol service registered");
|
|
@@ -2943,103 +3942,172 @@ var ObjectQLPlugin = class {
|
|
|
2943
3942
|
}
|
|
2944
3943
|
}
|
|
2945
3944
|
await this.ql?.init();
|
|
2946
|
-
await this.restoreMetadataFromDb(ctx);
|
|
2947
3945
|
await this.syncRegisteredSchemas(ctx);
|
|
2948
|
-
|
|
3946
|
+
if (this.projectId === void 0) {
|
|
3947
|
+
await this.restoreMetadataFromDb(ctx);
|
|
3948
|
+
} else {
|
|
3949
|
+
ctx.logger.info("Project kernel \u2014 skipping sys_metadata hydration (metadata sourced from artifact)");
|
|
3950
|
+
}
|
|
3951
|
+
await this.syncRegisteredSchemas(ctx);
|
|
3952
|
+
if (this.projectId === void 0) {
|
|
3953
|
+
await this.bridgeObjectsToMetadataService(ctx);
|
|
3954
|
+
}
|
|
2949
3955
|
this.registerAuditHooks(ctx);
|
|
2950
|
-
this.registerTenantMiddleware(ctx);
|
|
2951
3956
|
ctx.logger.info("ObjectQL engine started", {
|
|
2952
3957
|
driversRegistered: this.ql?.["drivers"]?.size || 0,
|
|
2953
3958
|
objectsRegistered: this.ql?.registry?.getAllObjects?.()?.length || 0
|
|
2954
3959
|
});
|
|
2955
3960
|
};
|
|
2956
|
-
if (
|
|
2957
|
-
this.ql =
|
|
2958
|
-
} else {
|
|
3961
|
+
if (qlOrOptions instanceof ObjectQL) {
|
|
3962
|
+
this.ql = qlOrOptions;
|
|
2959
3963
|
this.hostContext = hostContext;
|
|
3964
|
+
return;
|
|
2960
3965
|
}
|
|
3966
|
+
const opts = qlOrOptions ?? {};
|
|
3967
|
+
if (opts.ql) {
|
|
3968
|
+
this.ql = opts.ql;
|
|
3969
|
+
}
|
|
3970
|
+
this.hostContext = opts.hostContext ?? hostContext;
|
|
3971
|
+
this.projectId = opts.projectId;
|
|
2961
3972
|
}
|
|
2962
3973
|
/**
|
|
2963
3974
|
* Register built-in audit hooks for auto-stamping created_by/updated_by
|
|
2964
|
-
* and fetching previousData for update/delete operations.
|
|
3975
|
+
* and fetching previousData for update/delete operations. These are
|
|
3976
|
+
* declared as canonical `Hook` metadata and bound through the same
|
|
3977
|
+
* `bindHooksToEngine` path used by `defineStack({ hooks })`, so the
|
|
3978
|
+
* engine's built-ins flow through the same rails as user code
|
|
3979
|
+
* (dogfooding the protocol).
|
|
2965
3980
|
*/
|
|
2966
3981
|
registerAuditHooks(ctx) {
|
|
2967
3982
|
if (!this.ql) return;
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
3983
|
+
const stamp = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
3984
|
+
const hasField = (objectName, field) => {
|
|
3985
|
+
try {
|
|
3986
|
+
const schema = this.ql?.getSchema?.(objectName);
|
|
3987
|
+
if (!schema || typeof schema !== "object") return false;
|
|
3988
|
+
const fields = schema.fields;
|
|
3989
|
+
if (!fields || typeof fields !== "object") return false;
|
|
3990
|
+
return Object.prototype.hasOwnProperty.call(fields, field);
|
|
3991
|
+
} catch {
|
|
3992
|
+
return false;
|
|
3993
|
+
}
|
|
3994
|
+
};
|
|
3995
|
+
const applyToRecord = (record, objectName, session, isInsert) => {
|
|
3996
|
+
const now = stamp();
|
|
3997
|
+
if (isInsert) {
|
|
3998
|
+
record.created_at = record.created_at ?? now;
|
|
3999
|
+
}
|
|
4000
|
+
record.updated_at = now;
|
|
4001
|
+
if (session?.userId) {
|
|
4002
|
+
if (isInsert && hasField(objectName, "created_by")) {
|
|
4003
|
+
record.created_by = record.created_by ?? session.userId;
|
|
4004
|
+
}
|
|
4005
|
+
if (hasField(objectName, "updated_by")) {
|
|
4006
|
+
record.updated_by = session.userId;
|
|
2979
4007
|
}
|
|
2980
4008
|
}
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
4009
|
+
if (isInsert && session?.tenantId && hasField(objectName, "tenant_id")) {
|
|
4010
|
+
record.tenant_id = record.tenant_id ?? session.tenantId;
|
|
4011
|
+
}
|
|
4012
|
+
};
|
|
4013
|
+
const stampData = (data, objectName, session, isInsert) => {
|
|
4014
|
+
if (Array.isArray(data)) {
|
|
4015
|
+
for (const row of data) {
|
|
4016
|
+
if (row && typeof row === "object") {
|
|
4017
|
+
applyToRecord(row, objectName, session, isInsert);
|
|
4018
|
+
}
|
|
2988
4019
|
}
|
|
4020
|
+
} else if (data && typeof data === "object") {
|
|
4021
|
+
applyToRecord(data, objectName, session, isInsert);
|
|
2989
4022
|
}
|
|
2990
|
-
}
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
4023
|
+
};
|
|
4024
|
+
const builtinHooks = [
|
|
4025
|
+
{
|
|
4026
|
+
name: "sys_stamp_audit_insert",
|
|
4027
|
+
object: "*",
|
|
4028
|
+
events: ["beforeInsert"],
|
|
4029
|
+
priority: 10,
|
|
4030
|
+
description: "Auto-stamp created_by / updated_by / created_at / updated_at / tenant_id on insert (only when the field exists on the object schema)",
|
|
4031
|
+
handler: async (hookCtx) => {
|
|
4032
|
+
if (hookCtx.input?.data) {
|
|
4033
|
+
stampData(hookCtx.input.data, hookCtx.object, hookCtx.session, true);
|
|
4034
|
+
}
|
|
4035
|
+
}
|
|
4036
|
+
},
|
|
4037
|
+
{
|
|
4038
|
+
name: "sys_stamp_audit_update",
|
|
4039
|
+
object: "*",
|
|
4040
|
+
events: ["beforeUpdate"],
|
|
4041
|
+
priority: 10,
|
|
4042
|
+
description: "Auto-stamp updated_by / updated_at on update (only when the field exists on the object schema)",
|
|
4043
|
+
handler: async (hookCtx) => {
|
|
4044
|
+
if (hookCtx.input?.data) {
|
|
4045
|
+
stampData(hookCtx.input.data, hookCtx.object, hookCtx.session, false);
|
|
4046
|
+
}
|
|
4047
|
+
}
|
|
4048
|
+
},
|
|
4049
|
+
{
|
|
4050
|
+
name: "sys_fetch_previous_update",
|
|
4051
|
+
object: "*",
|
|
4052
|
+
events: ["beforeUpdate"],
|
|
4053
|
+
priority: 5,
|
|
4054
|
+
description: "Auto-fetch the previous record for update hooks",
|
|
4055
|
+
handler: async (hookCtx) => {
|
|
4056
|
+
if (hookCtx.input?.id && !hookCtx.previous) {
|
|
4057
|
+
try {
|
|
4058
|
+
const existing = await this.ql.findOne(hookCtx.object, {
|
|
4059
|
+
where: { id: hookCtx.input.id }
|
|
4060
|
+
});
|
|
4061
|
+
if (existing) hookCtx.previous = existing;
|
|
4062
|
+
} catch (_e) {
|
|
4063
|
+
}
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
4066
|
+
},
|
|
4067
|
+
{
|
|
4068
|
+
name: "sys_fetch_previous_delete",
|
|
4069
|
+
object: "*",
|
|
4070
|
+
events: ["beforeDelete"],
|
|
4071
|
+
priority: 5,
|
|
4072
|
+
description: "Auto-fetch the previous record for delete hooks",
|
|
4073
|
+
handler: async (hookCtx) => {
|
|
4074
|
+
if (hookCtx.input?.id && !hookCtx.previous) {
|
|
4075
|
+
try {
|
|
4076
|
+
const existing = await this.ql.findOne(hookCtx.object, {
|
|
4077
|
+
where: { id: hookCtx.input.id }
|
|
4078
|
+
});
|
|
4079
|
+
if (existing) hookCtx.previous = existing;
|
|
4080
|
+
} catch (_e) {
|
|
4081
|
+
}
|
|
2999
4082
|
}
|
|
3000
|
-
} catch (_e) {
|
|
3001
4083
|
}
|
|
3002
4084
|
}
|
|
3003
|
-
|
|
3004
|
-
this.ql.
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
4085
|
+
];
|
|
4086
|
+
if (typeof this.ql.bindHooks === "function") {
|
|
4087
|
+
this.ql.bindHooks(builtinHooks, { packageId: "sys:audit" });
|
|
4088
|
+
} else {
|
|
4089
|
+
for (const h of builtinHooks) {
|
|
4090
|
+
for (const event of h.events) {
|
|
4091
|
+
this.ql.registerHook(event, h.handler, {
|
|
4092
|
+
object: h.object,
|
|
4093
|
+
priority: h.priority,
|
|
4094
|
+
packageId: "sys:audit"
|
|
3009
4095
|
});
|
|
3010
|
-
if (existing) {
|
|
3011
|
-
hookCtx.previous = existing;
|
|
3012
|
-
}
|
|
3013
|
-
} catch (_e) {
|
|
3014
4096
|
}
|
|
3015
4097
|
}
|
|
3016
|
-
}
|
|
3017
|
-
ctx.logger.debug("Audit hooks registered (created_by/updated_by, previousData)");
|
|
4098
|
+
}
|
|
4099
|
+
ctx.logger.debug("Audit hooks registered via binder (created_by/updated_by, previousData)");
|
|
3018
4100
|
}
|
|
3019
4101
|
/**
|
|
3020
|
-
*
|
|
3021
|
-
*
|
|
4102
|
+
* Tenant isolation moved to `@objectstack/plugin-security`'s
|
|
4103
|
+
* `member_default` permission set RLS
|
|
4104
|
+
* (`organization_id = current_user.organization_id`, with
|
|
4105
|
+
* field-existence guards). The legacy `registerTenantMiddleware`
|
|
4106
|
+
* method was removed because it (a) collided with SecurityPlugin's
|
|
4107
|
+
* RLS pipeline and (b) blindly filtered tables that don't have a
|
|
4108
|
+
* `tenant_id` column (e.g. `sys_organization`), returning 0 rows
|
|
4109
|
+
* instead of all rows.
|
|
3022
4110
|
*/
|
|
3023
|
-
registerTenantMiddleware(ctx) {
|
|
3024
|
-
if (!this.ql) return;
|
|
3025
|
-
this.ql.registerMiddleware(async (opCtx, next) => {
|
|
3026
|
-
if (!opCtx.context?.tenantId || opCtx.context?.isSystem) {
|
|
3027
|
-
return next();
|
|
3028
|
-
}
|
|
3029
|
-
if (["find", "findOne", "count", "aggregate"].includes(opCtx.operation)) {
|
|
3030
|
-
if (opCtx.ast) {
|
|
3031
|
-
const tenantFilter = { tenant_id: opCtx.context.tenantId };
|
|
3032
|
-
if (opCtx.ast.where) {
|
|
3033
|
-
opCtx.ast.where = { $and: [opCtx.ast.where, tenantFilter] };
|
|
3034
|
-
} else {
|
|
3035
|
-
opCtx.ast.where = tenantFilter;
|
|
3036
|
-
}
|
|
3037
|
-
}
|
|
3038
|
-
}
|
|
3039
|
-
await next();
|
|
3040
|
-
});
|
|
3041
|
-
ctx.logger.debug("Tenant isolation middleware registered");
|
|
3042
|
-
}
|
|
3043
4111
|
/**
|
|
3044
4112
|
* Synchronize all registered object schemas to the database.
|
|
3045
4113
|
*
|
|
@@ -3078,7 +4146,7 @@ var ObjectQLPlugin = class {
|
|
|
3078
4146
|
skipped++;
|
|
3079
4147
|
continue;
|
|
3080
4148
|
}
|
|
3081
|
-
const tableName =
|
|
4149
|
+
const tableName = import_system2.StorageNameMapping.resolveTableName(obj);
|
|
3082
4150
|
let group = driverGroups.get(driver);
|
|
3083
4151
|
if (!group) {
|
|
3084
4152
|
group = [];
|
|
@@ -3235,13 +4303,20 @@ var ObjectQLPlugin = class {
|
|
|
3235
4303
|
*/
|
|
3236
4304
|
async loadMetadataFromService(metadataService, ctx) {
|
|
3237
4305
|
ctx.logger.info("Syncing metadata from external service into ObjectQL registry...");
|
|
3238
|
-
const metadataTypes = ["object", "view", "app", "flow", "workflow", "function"];
|
|
4306
|
+
const metadataTypes = ["object", "view", "app", "flow", "workflow", "function", "hook"];
|
|
3239
4307
|
let totalLoaded = 0;
|
|
3240
4308
|
for (const type of metadataTypes) {
|
|
3241
4309
|
try {
|
|
3242
4310
|
if (typeof metadataService.loadMany === "function") {
|
|
3243
4311
|
const items = await metadataService.loadMany(type);
|
|
3244
4312
|
if (items && items.length > 0) {
|
|
4313
|
+
if (type === "function" && this.ql && typeof this.ql.registerFunction === "function") {
|
|
4314
|
+
for (const item of items) {
|
|
4315
|
+
if (item?.name && typeof item.handler === "function") {
|
|
4316
|
+
this.ql.registerFunction(item.name, item.handler, "metadata-service");
|
|
4317
|
+
}
|
|
4318
|
+
}
|
|
4319
|
+
}
|
|
3245
4320
|
items.forEach((item) => {
|
|
3246
4321
|
const keyField = item.id ? "id" : "name";
|
|
3247
4322
|
if (type === "object" && this.ql) {
|
|
@@ -3251,6 +4326,11 @@ var ObjectQLPlugin = class {
|
|
|
3251
4326
|
this.ql.registry.registerItem(type, item, keyField);
|
|
3252
4327
|
}
|
|
3253
4328
|
});
|
|
4329
|
+
if (type === "hook" && this.ql && typeof this.ql.bindHooks === "function") {
|
|
4330
|
+
this.ql.bindHooks(items, {
|
|
4331
|
+
packageId: "metadata-service"
|
|
4332
|
+
});
|
|
4333
|
+
}
|
|
3254
4334
|
totalLoaded += items.length;
|
|
3255
4335
|
ctx.logger.info(`Synced ${items.length} ${type}(s) from metadata service`);
|
|
3256
4336
|
}
|
|
@@ -3367,6 +4447,7 @@ function convertIntrospectedSchemaToObjects(introspectedSchema, options) {
|
|
|
3367
4447
|
0 && (module.exports = {
|
|
3368
4448
|
DEFAULT_EXTENDER_PRIORITY,
|
|
3369
4449
|
DEFAULT_OWNER_PRIORITY,
|
|
4450
|
+
InMemoryHookMetricsRecorder,
|
|
3370
4451
|
MetadataFacade,
|
|
3371
4452
|
ObjectQL,
|
|
3372
4453
|
ObjectQLPlugin,
|
|
@@ -3375,10 +4456,14 @@ function convertIntrospectedSchemaToObjects(introspectedSchema, options) {
|
|
|
3375
4456
|
RESERVED_NAMESPACES,
|
|
3376
4457
|
SchemaRegistry,
|
|
3377
4458
|
ScopedContext,
|
|
4459
|
+
applySystemFields,
|
|
4460
|
+
bindHooksToEngine,
|
|
3378
4461
|
computeFQN,
|
|
3379
4462
|
convertIntrospectedSchemaToObjects,
|
|
3380
4463
|
createObjectQLKernel,
|
|
4464
|
+
noopHookMetricsRecorder,
|
|
3381
4465
|
parseFQN,
|
|
3382
|
-
toTitleCase
|
|
4466
|
+
toTitleCase,
|
|
4467
|
+
wrapDeclarativeHook
|
|
3383
4468
|
});
|
|
3384
4469
|
//# sourceMappingURL=index.js.map
|