@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.mjs
CHANGED
|
@@ -5,11 +5,8 @@ import { AppSchema } from "@objectstack/spec/ui";
|
|
|
5
5
|
var RESERVED_NAMESPACES = /* @__PURE__ */ new Set(["base", "system"]);
|
|
6
6
|
var DEFAULT_OWNER_PRIORITY = 100;
|
|
7
7
|
var DEFAULT_EXTENDER_PRIORITY = 200;
|
|
8
|
-
function computeFQN(
|
|
9
|
-
|
|
10
|
-
return shortName;
|
|
11
|
-
}
|
|
12
|
-
return `${namespace}__${shortName}`;
|
|
8
|
+
function computeFQN(_namespace, shortName) {
|
|
9
|
+
return shortName;
|
|
13
10
|
}
|
|
14
11
|
function parseFQN(fqn) {
|
|
15
12
|
const idx = fqn.indexOf("__");
|
|
@@ -37,14 +34,64 @@ function mergeObjectDefinitions(base, extension) {
|
|
|
37
34
|
if (extension.description !== void 0) merged.description = extension.description;
|
|
38
35
|
return merged;
|
|
39
36
|
}
|
|
37
|
+
function applySystemFields(schema, opts) {
|
|
38
|
+
if (schema.systemFields === false) return schema;
|
|
39
|
+
if (schema.managedBy) return schema;
|
|
40
|
+
const sf = typeof schema.systemFields === "object" && schema.systemFields !== null ? schema.systemFields : void 0;
|
|
41
|
+
const wantTenant = opts.multiTenant && sf?.tenant !== false;
|
|
42
|
+
const additions = {};
|
|
43
|
+
if (wantTenant && !schema.fields?.organization_id) {
|
|
44
|
+
additions.organization_id = {
|
|
45
|
+
type: "lookup",
|
|
46
|
+
reference: "sys_organization",
|
|
47
|
+
label: "Organization",
|
|
48
|
+
required: false,
|
|
49
|
+
indexed: true,
|
|
50
|
+
hidden: true,
|
|
51
|
+
readonly: true,
|
|
52
|
+
description: "Tenant scope (auto-populated by SecurityPlugin on insert)."
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (Object.keys(additions).length === 0) return schema;
|
|
56
|
+
return {
|
|
57
|
+
...schema,
|
|
58
|
+
fields: { ...additions, ...schema.fields ?? {} }
|
|
59
|
+
};
|
|
60
|
+
}
|
|
40
61
|
var SchemaRegistry = class {
|
|
41
|
-
|
|
62
|
+
constructor(options = {}) {
|
|
63
|
+
// ==========================================
|
|
64
|
+
// Logging control
|
|
65
|
+
// ==========================================
|
|
66
|
+
/** Controls verbosity of registry console messages. Default: 'info'. */
|
|
67
|
+
this._logLevel = "info";
|
|
68
|
+
// ==========================================
|
|
69
|
+
// Object-specific storage (Ownership Model)
|
|
70
|
+
// ==========================================
|
|
71
|
+
/** FQN → Contributor[] (all packages that own/extend this object) */
|
|
72
|
+
this.objectContributors = /* @__PURE__ */ new Map();
|
|
73
|
+
/** FQN → Merged ServiceObject (cached, invalidated on changes) */
|
|
74
|
+
this.mergedObjectCache = /* @__PURE__ */ new Map();
|
|
75
|
+
/** Namespace → Set<PackageId> (multiple packages can share a namespace) */
|
|
76
|
+
this.namespaceRegistry = /* @__PURE__ */ new Map();
|
|
77
|
+
// ==========================================
|
|
78
|
+
// Generic metadata storage (non-object types)
|
|
79
|
+
// ==========================================
|
|
80
|
+
/** Type → Name/ID → MetadataItem */
|
|
81
|
+
this.metadata = /* @__PURE__ */ new Map();
|
|
82
|
+
if (options.multiTenant !== void 0) {
|
|
83
|
+
this.multiTenant = options.multiTenant;
|
|
84
|
+
} else {
|
|
85
|
+
this.multiTenant = String(process.env.OS_MULTI_TENANT ?? "true").toLowerCase() !== "false";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
get logLevel() {
|
|
42
89
|
return this._logLevel;
|
|
43
90
|
}
|
|
44
|
-
|
|
91
|
+
set logLevel(level) {
|
|
45
92
|
this._logLevel = level;
|
|
46
93
|
}
|
|
47
|
-
|
|
94
|
+
log(msg) {
|
|
48
95
|
if (this._logLevel === "silent" || this._logLevel === "error" || this._logLevel === "warn") return;
|
|
49
96
|
console.log(msg);
|
|
50
97
|
}
|
|
@@ -55,7 +102,7 @@ var SchemaRegistry = class {
|
|
|
55
102
|
* Register a namespace for a package.
|
|
56
103
|
* Multiple packages can share the same namespace (e.g. 'sys').
|
|
57
104
|
*/
|
|
58
|
-
|
|
105
|
+
registerNamespace(namespace, packageId) {
|
|
59
106
|
if (!namespace) return;
|
|
60
107
|
let owners = this.namespaceRegistry.get(namespace);
|
|
61
108
|
if (!owners) {
|
|
@@ -68,7 +115,7 @@ var SchemaRegistry = class {
|
|
|
68
115
|
/**
|
|
69
116
|
* Unregister a namespace when a package is uninstalled.
|
|
70
117
|
*/
|
|
71
|
-
|
|
118
|
+
unregisterNamespace(namespace, packageId) {
|
|
72
119
|
const owners = this.namespaceRegistry.get(namespace);
|
|
73
120
|
if (owners) {
|
|
74
121
|
owners.delete(packageId);
|
|
@@ -81,7 +128,7 @@ var SchemaRegistry = class {
|
|
|
81
128
|
/**
|
|
82
129
|
* Get the packages that use a namespace.
|
|
83
130
|
*/
|
|
84
|
-
|
|
131
|
+
getNamespaceOwner(namespace) {
|
|
85
132
|
const owners = this.namespaceRegistry.get(namespace);
|
|
86
133
|
if (!owners || owners.size === 0) return void 0;
|
|
87
134
|
return owners.values().next().value;
|
|
@@ -89,7 +136,7 @@ var SchemaRegistry = class {
|
|
|
89
136
|
/**
|
|
90
137
|
* Get all packages that share a namespace.
|
|
91
138
|
*/
|
|
92
|
-
|
|
139
|
+
getNamespaceOwners(namespace) {
|
|
93
140
|
const owners = this.namespaceRegistry.get(namespace);
|
|
94
141
|
return owners ? Array.from(owners) : [];
|
|
95
142
|
}
|
|
@@ -107,7 +154,8 @@ var SchemaRegistry = class {
|
|
|
107
154
|
*
|
|
108
155
|
* @throws Error if trying to 'own' an object that already has an owner
|
|
109
156
|
*/
|
|
110
|
-
|
|
157
|
+
registerObject(schema, packageId, namespace, ownership = "own", priority = ownership === "own" ? DEFAULT_OWNER_PRIORITY : DEFAULT_EXTENDER_PRIORITY) {
|
|
158
|
+
schema = applySystemFields(schema, { multiTenant: this.multiTenant });
|
|
111
159
|
const shortName = schema.name;
|
|
112
160
|
const fqn = computeFQN(namespace, shortName);
|
|
113
161
|
if (namespace) {
|
|
@@ -154,7 +202,7 @@ var SchemaRegistry = class {
|
|
|
154
202
|
* Resolve an object by FQN, merging all contributions.
|
|
155
203
|
* Returns the merged object or undefined if not found.
|
|
156
204
|
*/
|
|
157
|
-
|
|
205
|
+
resolveObject(fqn) {
|
|
158
206
|
const cached = this.mergedObjectCache.get(fqn);
|
|
159
207
|
if (cached) return cached;
|
|
160
208
|
const contributors = this.objectContributors.get(fqn);
|
|
@@ -176,38 +224,42 @@ var SchemaRegistry = class {
|
|
|
176
224
|
return merged;
|
|
177
225
|
}
|
|
178
226
|
/**
|
|
179
|
-
* Get object by name (
|
|
227
|
+
* Get object by name (short name canonical, FQN supported for disambiguation).
|
|
228
|
+
*
|
|
229
|
+
* Short names are canonical for user code, AI generation, and most lookups.
|
|
230
|
+
* FQN is accepted as an explicit fallback for cross-package disambiguation
|
|
231
|
+
* when two packages contribute objects with the same short name.
|
|
180
232
|
*
|
|
181
233
|
* Resolution order:
|
|
182
|
-
* 1. Exact
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const direct = this.resolveObject(name);
|
|
190
|
-
if (direct) return direct;
|
|
234
|
+
* 1. Exact name match — the name IS the canonical key.
|
|
235
|
+
* If multiple packages contribute the same short name, a warning is logged
|
|
236
|
+
* and the first match wins — disambiguate by passing the FQN explicitly.
|
|
237
|
+
* 2. Legacy FQN match (e.g., 'crm__account') — backward compat.
|
|
238
|
+
*/
|
|
239
|
+
getObject(name) {
|
|
240
|
+
const matches = [];
|
|
191
241
|
for (const fqn of this.objectContributors.keys()) {
|
|
192
242
|
const { shortName } = parseFQN(fqn);
|
|
193
243
|
if (shortName === name) {
|
|
194
|
-
|
|
244
|
+
matches.push(fqn);
|
|
195
245
|
}
|
|
196
246
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
247
|
+
if (matches.length > 0) {
|
|
248
|
+
if (matches.length > 1) {
|
|
249
|
+
console.warn(
|
|
250
|
+
`[SchemaRegistry] Ambiguous short name "${name}" matches: ${matches.join(", ")}. Returning first match. Use FQN to disambiguate.`
|
|
251
|
+
);
|
|
201
252
|
}
|
|
253
|
+
return this.resolveObject(matches[0]);
|
|
202
254
|
}
|
|
203
|
-
return
|
|
255
|
+
return this.resolveObject(name);
|
|
204
256
|
}
|
|
205
257
|
/**
|
|
206
258
|
* Get all registered objects (merged).
|
|
207
259
|
*
|
|
208
260
|
* @param packageId - Optional filter: only objects contributed by this package
|
|
209
261
|
*/
|
|
210
|
-
|
|
262
|
+
getAllObjects(packageId) {
|
|
211
263
|
const results = [];
|
|
212
264
|
for (const fqn of this.objectContributors.keys()) {
|
|
213
265
|
if (packageId) {
|
|
@@ -226,13 +278,13 @@ var SchemaRegistry = class {
|
|
|
226
278
|
/**
|
|
227
279
|
* Get all contributors for an object.
|
|
228
280
|
*/
|
|
229
|
-
|
|
281
|
+
getObjectContributors(fqn) {
|
|
230
282
|
return this.objectContributors.get(fqn) || [];
|
|
231
283
|
}
|
|
232
284
|
/**
|
|
233
285
|
* Get the owner contributor for an object.
|
|
234
286
|
*/
|
|
235
|
-
|
|
287
|
+
getObjectOwner(fqn) {
|
|
236
288
|
const contributors = this.objectContributors.get(fqn);
|
|
237
289
|
return contributors?.find((c) => c.ownership === "own");
|
|
238
290
|
}
|
|
@@ -241,7 +293,7 @@ var SchemaRegistry = class {
|
|
|
241
293
|
*
|
|
242
294
|
* @throws Error if trying to uninstall an owner that has extenders
|
|
243
295
|
*/
|
|
244
|
-
|
|
296
|
+
unregisterObjectsByPackage(packageId, force = false) {
|
|
245
297
|
for (const [fqn, contributors] of this.objectContributors.entries()) {
|
|
246
298
|
const packageContribs = contributors.filter((c) => c.packageId === packageId);
|
|
247
299
|
for (const contrib of packageContribs) {
|
|
@@ -273,7 +325,7 @@ var SchemaRegistry = class {
|
|
|
273
325
|
/**
|
|
274
326
|
* Universal Register Method for non-object metadata.
|
|
275
327
|
*/
|
|
276
|
-
|
|
328
|
+
registerItem(type, item, keyField = "name", packageId) {
|
|
277
329
|
if (!this.metadata.has(type)) {
|
|
278
330
|
this.metadata.set(type, /* @__PURE__ */ new Map());
|
|
279
331
|
}
|
|
@@ -289,7 +341,7 @@ var SchemaRegistry = class {
|
|
|
289
341
|
}
|
|
290
342
|
const storageKey = packageId ? `${packageId}:${baseName}` : baseName;
|
|
291
343
|
if (collection.has(storageKey)) {
|
|
292
|
-
|
|
344
|
+
this.log(`[Registry] Overwriting ${type}: ${storageKey}`);
|
|
293
345
|
}
|
|
294
346
|
collection.set(storageKey, item);
|
|
295
347
|
this.log(`[Registry] Registered ${type}: ${storageKey}`);
|
|
@@ -297,7 +349,7 @@ var SchemaRegistry = class {
|
|
|
297
349
|
/**
|
|
298
350
|
* Validate Metadata against Spec Zod Schemas
|
|
299
351
|
*/
|
|
300
|
-
|
|
352
|
+
validate(type, item) {
|
|
301
353
|
if (type === "object") {
|
|
302
354
|
return ObjectSchema.parse(item);
|
|
303
355
|
}
|
|
@@ -315,7 +367,7 @@ var SchemaRegistry = class {
|
|
|
315
367
|
/**
|
|
316
368
|
* Universal Unregister Method
|
|
317
369
|
*/
|
|
318
|
-
|
|
370
|
+
unregisterItem(type, name) {
|
|
319
371
|
const collection = this.metadata.get(type);
|
|
320
372
|
if (!collection) {
|
|
321
373
|
console.warn(`[Registry] Attempted to unregister non-existent ${type}: ${name}`);
|
|
@@ -338,7 +390,7 @@ var SchemaRegistry = class {
|
|
|
338
390
|
/**
|
|
339
391
|
* Universal Get Method
|
|
340
392
|
*/
|
|
341
|
-
|
|
393
|
+
getItem(type, name) {
|
|
342
394
|
if (type === "object" || type === "objects") {
|
|
343
395
|
return this.getObject(name);
|
|
344
396
|
}
|
|
@@ -354,7 +406,7 @@ var SchemaRegistry = class {
|
|
|
354
406
|
/**
|
|
355
407
|
* Universal List Method
|
|
356
408
|
*/
|
|
357
|
-
|
|
409
|
+
listItems(type, packageId) {
|
|
358
410
|
if (type === "object" || type === "objects") {
|
|
359
411
|
return this.getAllObjects(packageId);
|
|
360
412
|
}
|
|
@@ -367,7 +419,7 @@ var SchemaRegistry = class {
|
|
|
367
419
|
/**
|
|
368
420
|
* Get all registered metadata types (Kinds)
|
|
369
421
|
*/
|
|
370
|
-
|
|
422
|
+
getRegisteredTypes() {
|
|
371
423
|
const types = Array.from(this.metadata.keys());
|
|
372
424
|
if (!types.includes("object") && this.objectContributors.size > 0) {
|
|
373
425
|
types.push("object");
|
|
@@ -377,7 +429,7 @@ var SchemaRegistry = class {
|
|
|
377
429
|
// ==========================================
|
|
378
430
|
// Package Management
|
|
379
431
|
// ==========================================
|
|
380
|
-
|
|
432
|
+
installPackage(manifest, settings) {
|
|
381
433
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
382
434
|
const pkg = {
|
|
383
435
|
manifest,
|
|
@@ -401,7 +453,7 @@ var SchemaRegistry = class {
|
|
|
401
453
|
this.log(`[Registry] Installed package: ${manifest.id} (${manifest.name})`);
|
|
402
454
|
return pkg;
|
|
403
455
|
}
|
|
404
|
-
|
|
456
|
+
uninstallPackage(id) {
|
|
405
457
|
const pkg = this.getPackage(id);
|
|
406
458
|
if (!pkg) {
|
|
407
459
|
console.warn(`[Registry] Package not found for uninstall: ${id}`);
|
|
@@ -419,13 +471,13 @@ var SchemaRegistry = class {
|
|
|
419
471
|
}
|
|
420
472
|
return false;
|
|
421
473
|
}
|
|
422
|
-
|
|
474
|
+
getPackage(id) {
|
|
423
475
|
return this.metadata.get("package")?.get(id);
|
|
424
476
|
}
|
|
425
|
-
|
|
477
|
+
getAllPackages() {
|
|
426
478
|
return this.listItems("package");
|
|
427
479
|
}
|
|
428
|
-
|
|
480
|
+
enablePackage(id) {
|
|
429
481
|
const pkg = this.getPackage(id);
|
|
430
482
|
if (pkg) {
|
|
431
483
|
pkg.enabled = true;
|
|
@@ -436,7 +488,7 @@ var SchemaRegistry = class {
|
|
|
436
488
|
}
|
|
437
489
|
return pkg;
|
|
438
490
|
}
|
|
439
|
-
|
|
491
|
+
disablePackage(id) {
|
|
440
492
|
const pkg = this.getPackage(id);
|
|
441
493
|
if (pkg) {
|
|
442
494
|
pkg.enabled = false;
|
|
@@ -450,31 +502,31 @@ var SchemaRegistry = class {
|
|
|
450
502
|
// ==========================================
|
|
451
503
|
// App Helpers
|
|
452
504
|
// ==========================================
|
|
453
|
-
|
|
505
|
+
registerApp(app, packageId) {
|
|
454
506
|
this.registerItem("app", app, "name", packageId);
|
|
455
507
|
}
|
|
456
|
-
|
|
508
|
+
getApp(name) {
|
|
457
509
|
return this.getItem("app", name);
|
|
458
510
|
}
|
|
459
|
-
|
|
511
|
+
getAllApps() {
|
|
460
512
|
return this.listItems("app");
|
|
461
513
|
}
|
|
462
514
|
// ==========================================
|
|
463
515
|
// Plugin Helpers
|
|
464
516
|
// ==========================================
|
|
465
|
-
|
|
517
|
+
registerPlugin(manifest) {
|
|
466
518
|
this.registerItem("plugin", manifest, "id");
|
|
467
519
|
}
|
|
468
|
-
|
|
520
|
+
getAllPlugins() {
|
|
469
521
|
return this.listItems("plugin");
|
|
470
522
|
}
|
|
471
523
|
// ==========================================
|
|
472
524
|
// Kind Helpers
|
|
473
525
|
// ==========================================
|
|
474
|
-
|
|
526
|
+
registerKind(kind) {
|
|
475
527
|
this.registerItem("kind", kind, "id");
|
|
476
528
|
}
|
|
477
|
-
|
|
529
|
+
getAllKinds() {
|
|
478
530
|
return this.listItems("kind");
|
|
479
531
|
}
|
|
480
532
|
// ==========================================
|
|
@@ -483,7 +535,7 @@ var SchemaRegistry = class {
|
|
|
483
535
|
/**
|
|
484
536
|
* Clear all registry state. Use only for testing.
|
|
485
537
|
*/
|
|
486
|
-
|
|
538
|
+
reset() {
|
|
487
539
|
this.objectContributors.clear();
|
|
488
540
|
this.mergedObjectCache.clear();
|
|
489
541
|
this.namespaceRegistry.clear();
|
|
@@ -491,25 +543,6 @@ var SchemaRegistry = class {
|
|
|
491
543
|
this.log("[Registry] Reset complete");
|
|
492
544
|
}
|
|
493
545
|
};
|
|
494
|
-
// ==========================================
|
|
495
|
-
// Logging control
|
|
496
|
-
// ==========================================
|
|
497
|
-
/** Controls verbosity of registry console messages. Default: 'info'. */
|
|
498
|
-
SchemaRegistry._logLevel = "info";
|
|
499
|
-
// ==========================================
|
|
500
|
-
// Object-specific storage (Ownership Model)
|
|
501
|
-
// ==========================================
|
|
502
|
-
/** FQN → Contributor[] (all packages that own/extend this object) */
|
|
503
|
-
SchemaRegistry.objectContributors = /* @__PURE__ */ new Map();
|
|
504
|
-
/** FQN → Merged ServiceObject (cached, invalidated on changes) */
|
|
505
|
-
SchemaRegistry.mergedObjectCache = /* @__PURE__ */ new Map();
|
|
506
|
-
/** Namespace → Set<PackageId> (multiple packages can share a namespace) */
|
|
507
|
-
SchemaRegistry.namespaceRegistry = /* @__PURE__ */ new Map();
|
|
508
|
-
// ==========================================
|
|
509
|
-
// Generic metadata storage (non-object types)
|
|
510
|
-
// ==========================================
|
|
511
|
-
/** Type → Name/ID → MetadataItem */
|
|
512
|
-
SchemaRegistry.metadata = /* @__PURE__ */ new Map();
|
|
513
546
|
|
|
514
547
|
// src/protocol.ts
|
|
515
548
|
import { parseFilterAST, isFilterAST } from "@objectstack/spec/data";
|
|
@@ -541,10 +574,20 @@ var SERVICE_CONFIG = {
|
|
|
541
574
|
search: { route: "/api/v1/search", plugin: "plugin-search" }
|
|
542
575
|
};
|
|
543
576
|
var ObjectStackProtocolImplementation = class {
|
|
544
|
-
constructor(engine, getServicesRegistry, getFeedService) {
|
|
577
|
+
constructor(engine, getServicesRegistry, getFeedService, projectId) {
|
|
545
578
|
this.engine = engine;
|
|
546
579
|
this.getServicesRegistry = getServicesRegistry;
|
|
547
580
|
this.getFeedService = getFeedService;
|
|
581
|
+
this.projectId = projectId;
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Exposes the project scope the protocol is bound to. Consumers like
|
|
585
|
+
* the HTTP dispatcher use this to decide whether to trust the process-
|
|
586
|
+
* wide SchemaRegistry or whether they must route a read through the
|
|
587
|
+
* protocol's project_id-filtered lookup.
|
|
588
|
+
*/
|
|
589
|
+
getProjectId() {
|
|
590
|
+
return this.projectId;
|
|
548
591
|
}
|
|
549
592
|
requireFeedService() {
|
|
550
593
|
const svc = this.getFeedService?.();
|
|
@@ -641,7 +684,7 @@ var ObjectStackProtocolImplementation = class {
|
|
|
641
684
|
};
|
|
642
685
|
}
|
|
643
686
|
async getMetaTypes() {
|
|
644
|
-
const schemaTypes =
|
|
687
|
+
const schemaTypes = this.engine.registry.getRegisteredTypes();
|
|
645
688
|
let runtimeTypes = [];
|
|
646
689
|
try {
|
|
647
690
|
const services = this.getServicesRegistry?.();
|
|
@@ -656,47 +699,67 @@ var ObjectStackProtocolImplementation = class {
|
|
|
656
699
|
}
|
|
657
700
|
async getMetaItems(request) {
|
|
658
701
|
const { packageId } = request;
|
|
659
|
-
let items =
|
|
660
|
-
if (
|
|
661
|
-
|
|
662
|
-
if (
|
|
702
|
+
let items = [];
|
|
703
|
+
if (this.projectId === void 0) {
|
|
704
|
+
items = [...this.engine.registry.listItems(request.type, packageId)];
|
|
705
|
+
if (items.length === 0) {
|
|
706
|
+
const alt = PLURAL_TO_SINGULAR[request.type] ?? SINGULAR_TO_PLURAL[request.type];
|
|
707
|
+
if (alt) items = [...this.engine.registry.listItems(alt, packageId)];
|
|
708
|
+
}
|
|
709
|
+
} else {
|
|
710
|
+
items = [...this.engine.registry.listItems(request.type, packageId)];
|
|
711
|
+
if (items.length === 0) {
|
|
712
|
+
const alt = PLURAL_TO_SINGULAR[request.type] ?? SINGULAR_TO_PLURAL[request.type];
|
|
713
|
+
if (alt) items = [...this.engine.registry.listItems(alt, packageId)];
|
|
714
|
+
}
|
|
663
715
|
}
|
|
664
|
-
if (
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
});
|
|
689
|
-
}
|
|
716
|
+
if (this.projectId === void 0) try {
|
|
717
|
+
const whereClause = {
|
|
718
|
+
type: request.type,
|
|
719
|
+
state: "active",
|
|
720
|
+
// Always filter by project_id: project kernels use their projectId,
|
|
721
|
+
// control-plane kernels use NULL (global scope only).
|
|
722
|
+
project_id: this.projectId ?? null
|
|
723
|
+
};
|
|
724
|
+
if (packageId) whereClause._packageId = packageId;
|
|
725
|
+
let records = await this.engine.find("sys_metadata", { where: whereClause });
|
|
726
|
+
if (!records || records.length === 0) {
|
|
727
|
+
const alt = PLURAL_TO_SINGULAR[request.type] ?? SINGULAR_TO_PLURAL[request.type];
|
|
728
|
+
if (alt) {
|
|
729
|
+
const altWhere = { type: alt, state: "active", project_id: this.projectId ?? null };
|
|
730
|
+
if (packageId) altWhere._packageId = packageId;
|
|
731
|
+
records = await this.engine.find("sys_metadata", { where: altWhere });
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
if (records && records.length > 0) {
|
|
735
|
+
const byName = /* @__PURE__ */ new Map();
|
|
736
|
+
for (const existing of items) {
|
|
737
|
+
const entry = existing;
|
|
738
|
+
if (entry && typeof entry === "object" && "name" in entry) {
|
|
739
|
+
byName.set(entry.name, entry);
|
|
690
740
|
}
|
|
691
741
|
}
|
|
692
|
-
|
|
742
|
+
for (const record of records) {
|
|
743
|
+
const data = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
|
|
744
|
+
if (data && typeof data === "object" && "name" in data) {
|
|
745
|
+
byName.set(data.name, data);
|
|
746
|
+
}
|
|
747
|
+
if (this.projectId === void 0) {
|
|
748
|
+
this.engine.registry.registerItem(request.type, data, "name");
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
items = Array.from(byName.values());
|
|
693
752
|
}
|
|
753
|
+
} catch {
|
|
694
754
|
}
|
|
695
755
|
try {
|
|
696
756
|
const services = this.getServicesRegistry?.();
|
|
697
757
|
const metadataService = services?.get("metadata");
|
|
698
758
|
if (metadataService && typeof metadataService.list === "function") {
|
|
699
|
-
|
|
759
|
+
let runtimeItems = await metadataService.list(request.type);
|
|
760
|
+
if (packageId && runtimeItems && runtimeItems.length > 0) {
|
|
761
|
+
runtimeItems = runtimeItems.filter((item) => item?._packageId === packageId);
|
|
762
|
+
}
|
|
700
763
|
if (runtimeItems && runtimeItems.length > 0) {
|
|
701
764
|
const itemMap = /* @__PURE__ */ new Map();
|
|
702
765
|
for (const item of items) {
|
|
@@ -722,28 +785,41 @@ var ObjectStackProtocolImplementation = class {
|
|
|
722
785
|
};
|
|
723
786
|
}
|
|
724
787
|
async getMetaItem(request) {
|
|
725
|
-
let item
|
|
726
|
-
if (
|
|
727
|
-
|
|
728
|
-
if (
|
|
788
|
+
let item;
|
|
789
|
+
if (this.projectId === void 0) {
|
|
790
|
+
item = this.engine.registry.getItem(request.type, request.name);
|
|
791
|
+
if (item === void 0) {
|
|
792
|
+
const alt = PLURAL_TO_SINGULAR[request.type] ?? SINGULAR_TO_PLURAL[request.type];
|
|
793
|
+
if (alt) item = this.engine.registry.getItem(alt, request.name);
|
|
794
|
+
}
|
|
729
795
|
}
|
|
730
|
-
if (item === void 0) {
|
|
796
|
+
if (item === void 0 && this.projectId === void 0) {
|
|
731
797
|
try {
|
|
798
|
+
const scopedWhere = {
|
|
799
|
+
type: request.type,
|
|
800
|
+
name: request.name,
|
|
801
|
+
state: "active"
|
|
802
|
+
};
|
|
803
|
+
scopedWhere.project_id = this.projectId ?? null;
|
|
732
804
|
const record = await this.engine.findOne("sys_metadata", {
|
|
733
|
-
where:
|
|
805
|
+
where: scopedWhere
|
|
734
806
|
});
|
|
735
807
|
if (record) {
|
|
736
808
|
item = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
|
|
737
|
-
|
|
809
|
+
if (this.projectId === void 0) {
|
|
810
|
+
this.engine.registry.registerItem(request.type, item, "name");
|
|
811
|
+
}
|
|
738
812
|
} else {
|
|
739
813
|
const alt = PLURAL_TO_SINGULAR[request.type] ?? SINGULAR_TO_PLURAL[request.type];
|
|
740
814
|
if (alt) {
|
|
741
|
-
const
|
|
742
|
-
|
|
743
|
-
});
|
|
815
|
+
const altWhere = { type: alt, name: request.name, state: "active" };
|
|
816
|
+
altWhere.project_id = this.projectId ?? null;
|
|
817
|
+
const altRecord = await this.engine.findOne("sys_metadata", { where: altWhere });
|
|
744
818
|
if (altRecord) {
|
|
745
819
|
item = typeof altRecord.metadata === "string" ? JSON.parse(altRecord.metadata) : altRecord.metadata;
|
|
746
|
-
|
|
820
|
+
if (this.projectId === void 0) {
|
|
821
|
+
this.engine.registry.registerItem(request.type, item, "name");
|
|
822
|
+
}
|
|
747
823
|
}
|
|
748
824
|
}
|
|
749
825
|
}
|
|
@@ -767,7 +843,7 @@ var ObjectStackProtocolImplementation = class {
|
|
|
767
843
|
};
|
|
768
844
|
}
|
|
769
845
|
async getUiView(request) {
|
|
770
|
-
const schema =
|
|
846
|
+
const schema = this.engine.registry.getObject(request.object);
|
|
771
847
|
if (!schema) throw new Error(`Object ${request.object} not found`);
|
|
772
848
|
const fields = schema.fields || {};
|
|
773
849
|
const fieldKeys = Object.keys(fields);
|
|
@@ -823,6 +899,9 @@ var ObjectStackProtocolImplementation = class {
|
|
|
823
899
|
}
|
|
824
900
|
async findData(request) {
|
|
825
901
|
const options = { ...request.query };
|
|
902
|
+
if (request.context !== void 0) {
|
|
903
|
+
options.context = request.context;
|
|
904
|
+
}
|
|
826
905
|
if (options.top != null) {
|
|
827
906
|
options.limit = Number(options.top);
|
|
828
907
|
delete options.top;
|
|
@@ -941,6 +1020,9 @@ var ObjectStackProtocolImplementation = class {
|
|
|
941
1020
|
const queryOptions = {
|
|
942
1021
|
where: { id: request.id }
|
|
943
1022
|
};
|
|
1023
|
+
if (request.context !== void 0) {
|
|
1024
|
+
queryOptions.context = request.context;
|
|
1025
|
+
}
|
|
944
1026
|
if (request.select) {
|
|
945
1027
|
queryOptions.fields = typeof request.select === "string" ? request.select.split(",").map((s) => s.trim()).filter(Boolean) : request.select;
|
|
946
1028
|
}
|
|
@@ -962,7 +1044,11 @@ var ObjectStackProtocolImplementation = class {
|
|
|
962
1044
|
throw new Error(`Record ${request.id} not found in ${request.object}`);
|
|
963
1045
|
}
|
|
964
1046
|
async createData(request) {
|
|
965
|
-
const result = await this.engine.insert(
|
|
1047
|
+
const result = await this.engine.insert(
|
|
1048
|
+
request.object,
|
|
1049
|
+
request.data,
|
|
1050
|
+
request.context !== void 0 ? { context: request.context } : void 0
|
|
1051
|
+
);
|
|
966
1052
|
return {
|
|
967
1053
|
object: request.object,
|
|
968
1054
|
id: result.id,
|
|
@@ -970,7 +1056,9 @@ var ObjectStackProtocolImplementation = class {
|
|
|
970
1056
|
};
|
|
971
1057
|
}
|
|
972
1058
|
async updateData(request) {
|
|
973
|
-
const
|
|
1059
|
+
const opts = { where: { id: request.id } };
|
|
1060
|
+
if (request.context !== void 0) opts.context = request.context;
|
|
1061
|
+
const result = await this.engine.update(request.object, request.data, opts);
|
|
974
1062
|
return {
|
|
975
1063
|
object: request.object,
|
|
976
1064
|
id: request.id,
|
|
@@ -978,7 +1066,9 @@ var ObjectStackProtocolImplementation = class {
|
|
|
978
1066
|
};
|
|
979
1067
|
}
|
|
980
1068
|
async deleteData(request) {
|
|
981
|
-
|
|
1069
|
+
const opts = { where: { id: request.id } };
|
|
1070
|
+
if (request.context !== void 0) opts.context = request.context;
|
|
1071
|
+
await this.engine.delete(request.object, opts);
|
|
982
1072
|
return {
|
|
983
1073
|
object: request.object,
|
|
984
1074
|
id: request.id,
|
|
@@ -990,10 +1080,10 @@ var ObjectStackProtocolImplementation = class {
|
|
|
990
1080
|
// ==========================================
|
|
991
1081
|
async getMetaItemCached(request) {
|
|
992
1082
|
try {
|
|
993
|
-
let item =
|
|
1083
|
+
let item = this.engine.registry.getItem(request.type, request.name);
|
|
994
1084
|
if (!item) {
|
|
995
1085
|
const alt = PLURAL_TO_SINGULAR[request.type] ?? SINGULAR_TO_PLURAL[request.type];
|
|
996
|
-
if (alt) item =
|
|
1086
|
+
if (alt) item = this.engine.registry.getItem(alt, request.name);
|
|
997
1087
|
}
|
|
998
1088
|
if (!item) {
|
|
999
1089
|
try {
|
|
@@ -1196,7 +1286,7 @@ var ObjectStackProtocolImplementation = class {
|
|
|
1196
1286
|
};
|
|
1197
1287
|
}
|
|
1198
1288
|
async getAnalyticsMeta(request) {
|
|
1199
|
-
const objects =
|
|
1289
|
+
const objects = this.engine.registry.listItems("object");
|
|
1200
1290
|
const cubeFilter = request?.cube;
|
|
1201
1291
|
const cubes = [];
|
|
1202
1292
|
for (const obj of objects) {
|
|
@@ -1300,11 +1390,43 @@ var ObjectStackProtocolImplementation = class {
|
|
|
1300
1390
|
if (!request.item) {
|
|
1301
1391
|
throw new Error("Item data is required");
|
|
1302
1392
|
}
|
|
1303
|
-
|
|
1393
|
+
if (this.projectId !== void 0) {
|
|
1394
|
+
this.engine.registry.registerItem(request.type, request.item, "name");
|
|
1395
|
+
if (request.type === "object" || request.type === "objects") {
|
|
1396
|
+
try {
|
|
1397
|
+
this.engine.registry.registerObject(request.item, "sys_metadata");
|
|
1398
|
+
} catch (err) {
|
|
1399
|
+
console.warn(
|
|
1400
|
+
`[Protocol] registerObject failed for ${request.name}: ${err?.message ?? err}`
|
|
1401
|
+
);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
return {
|
|
1405
|
+
success: true,
|
|
1406
|
+
message: "Saved to memory registry (project kernel \u2014 sys_metadata is control-plane only)"
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
this.engine.registry.registerItem(request.type, request.item, "name");
|
|
1410
|
+
if (request.type === "object" || request.type === "objects") {
|
|
1411
|
+
try {
|
|
1412
|
+
this.engine.registry.registerObject(request.item, "sys_metadata");
|
|
1413
|
+
} catch (err) {
|
|
1414
|
+
console.warn(
|
|
1415
|
+
`[Protocol] this.engine.registry.registerObject failed for ${request.name}: ${err?.message ?? err}`
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1304
1419
|
try {
|
|
1305
1420
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1421
|
+
const scopedWhere = {
|
|
1422
|
+
type: request.type,
|
|
1423
|
+
name: request.name
|
|
1424
|
+
};
|
|
1425
|
+
if (this.projectId !== void 0) {
|
|
1426
|
+
scopedWhere.project_id = this.projectId;
|
|
1427
|
+
}
|
|
1306
1428
|
const existing = await this.engine.findOne("sys_metadata", {
|
|
1307
|
-
where:
|
|
1429
|
+
where: scopedWhere
|
|
1308
1430
|
});
|
|
1309
1431
|
if (existing) {
|
|
1310
1432
|
await this.engine.update("sys_metadata", {
|
|
@@ -1316,17 +1438,24 @@ var ObjectStackProtocolImplementation = class {
|
|
|
1316
1438
|
});
|
|
1317
1439
|
} else {
|
|
1318
1440
|
const id = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `meta_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
1319
|
-
|
|
1441
|
+
const row = {
|
|
1320
1442
|
id,
|
|
1321
1443
|
name: request.name,
|
|
1322
1444
|
type: request.type,
|
|
1323
|
-
scope
|
|
1445
|
+
// `scope` tracks platform vs project authorship. With
|
|
1446
|
+
// project_id carries the project id, 'project' is the
|
|
1447
|
+
// honest label whenever we know we're inside one.
|
|
1448
|
+
scope: this.projectId !== void 0 ? "project" : "platform",
|
|
1324
1449
|
metadata: JSON.stringify(request.item),
|
|
1325
1450
|
state: "active",
|
|
1326
1451
|
version: 1,
|
|
1327
1452
|
created_at: now,
|
|
1328
1453
|
updated_at: now
|
|
1329
|
-
}
|
|
1454
|
+
};
|
|
1455
|
+
if (this.projectId !== void 0) {
|
|
1456
|
+
row.project_id = this.projectId;
|
|
1457
|
+
}
|
|
1458
|
+
await this.engine.insert("sys_metadata", row);
|
|
1330
1459
|
}
|
|
1331
1460
|
return {
|
|
1332
1461
|
success: true,
|
|
@@ -1347,20 +1476,25 @@ var ObjectStackProtocolImplementation = class {
|
|
|
1347
1476
|
* Safe to call repeatedly — idempotent (latest DB record wins).
|
|
1348
1477
|
*/
|
|
1349
1478
|
async loadMetaFromDb() {
|
|
1479
|
+
if (this.projectId !== void 0) {
|
|
1480
|
+
return { loaded: 0, errors: 0 };
|
|
1481
|
+
}
|
|
1350
1482
|
let loaded = 0;
|
|
1351
1483
|
let errors = 0;
|
|
1352
1484
|
try {
|
|
1353
|
-
const
|
|
1354
|
-
|
|
1355
|
-
|
|
1485
|
+
const where = {
|
|
1486
|
+
state: "active",
|
|
1487
|
+
project_id: this.projectId ?? null
|
|
1488
|
+
};
|
|
1489
|
+
const records = await this.engine.find("sys_metadata", { where });
|
|
1356
1490
|
for (const record of records) {
|
|
1357
1491
|
try {
|
|
1358
1492
|
const data = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
|
|
1359
1493
|
const normalizedType = PLURAL_TO_SINGULAR[record.type] ?? record.type;
|
|
1360
1494
|
if (normalizedType === "object") {
|
|
1361
|
-
|
|
1495
|
+
this.engine.registry.registerObject(data, record.packageId || "sys_metadata");
|
|
1362
1496
|
} else {
|
|
1363
|
-
|
|
1497
|
+
this.engine.registry.registerItem(normalizedType, data, "name");
|
|
1364
1498
|
}
|
|
1365
1499
|
loaded++;
|
|
1366
1500
|
} catch (e) {
|
|
@@ -1369,7 +1503,9 @@ var ObjectStackProtocolImplementation = class {
|
|
|
1369
1503
|
}
|
|
1370
1504
|
}
|
|
1371
1505
|
} catch (e) {
|
|
1372
|
-
|
|
1506
|
+
if (!/no such table/i.test(e.message ?? "")) {
|
|
1507
|
+
console.warn(`[Protocol] DB hydration skipped: ${e.message}`);
|
|
1508
|
+
}
|
|
1373
1509
|
}
|
|
1374
1510
|
return { loaded, errors };
|
|
1375
1511
|
}
|
|
@@ -1509,12 +1645,490 @@ var ObjectStackProtocolImplementation = class {
|
|
|
1509
1645
|
// src/engine.ts
|
|
1510
1646
|
import { ExecutionContextSchema } from "@objectstack/spec/kernel";
|
|
1511
1647
|
import { createLogger } from "@objectstack/core";
|
|
1512
|
-
import { CoreServiceName } from "@objectstack/spec/system";
|
|
1648
|
+
import { CoreServiceName, StorageNameMapping } from "@objectstack/spec/system";
|
|
1513
1649
|
import { pluralToSingular } from "@objectstack/spec/shared";
|
|
1650
|
+
import { ExpressionEngine as ExpressionEngine2 } from "@objectstack/formula";
|
|
1651
|
+
|
|
1652
|
+
// src/hook-wrappers.ts
|
|
1653
|
+
import { ExpressionEngine } from "@objectstack/formula";
|
|
1654
|
+
|
|
1655
|
+
// src/hook-metrics.ts
|
|
1656
|
+
var noopHookMetricsRecorder = {
|
|
1657
|
+
recordExecution: () => {
|
|
1658
|
+
},
|
|
1659
|
+
recordSkip: () => {
|
|
1660
|
+
},
|
|
1661
|
+
recordRetry: () => {
|
|
1662
|
+
}
|
|
1663
|
+
};
|
|
1664
|
+
var InMemoryHookMetricsRecorder = class {
|
|
1665
|
+
constructor() {
|
|
1666
|
+
this.executions = /* @__PURE__ */ new Map();
|
|
1667
|
+
this.skips = /* @__PURE__ */ new Map();
|
|
1668
|
+
this.retries = /* @__PURE__ */ new Map();
|
|
1669
|
+
}
|
|
1670
|
+
recordExecution(label, outcome, durationMs) {
|
|
1671
|
+
const key = `${label.hook}|${outcome}`;
|
|
1672
|
+
const cur = this.executions.get(key) ?? { count: 0, totalMs: 0 };
|
|
1673
|
+
cur.count += 1;
|
|
1674
|
+
cur.totalMs += Math.max(0, durationMs);
|
|
1675
|
+
this.executions.set(key, cur);
|
|
1676
|
+
}
|
|
1677
|
+
recordSkip(label, reason) {
|
|
1678
|
+
const key = `${label.hook}|${reason}`;
|
|
1679
|
+
this.skips.set(key, (this.skips.get(key) ?? 0) + 1);
|
|
1680
|
+
}
|
|
1681
|
+
recordRetry(label, _attempt) {
|
|
1682
|
+
this.retries.set(label.hook, (this.retries.get(label.hook) ?? 0) + 1);
|
|
1683
|
+
}
|
|
1684
|
+
snapshot() {
|
|
1685
|
+
return {
|
|
1686
|
+
executions: Array.from(this.executions, ([key, v]) => {
|
|
1687
|
+
const [hook, outcome] = key.split("|");
|
|
1688
|
+
return { hook, outcome, count: v.count, totalMs: v.totalMs };
|
|
1689
|
+
}),
|
|
1690
|
+
skips: Array.from(this.skips, ([key, count]) => {
|
|
1691
|
+
const [hook, reason] = key.split("|");
|
|
1692
|
+
return { hook, reason, count };
|
|
1693
|
+
}),
|
|
1694
|
+
retries: Array.from(this.retries, ([hook, count]) => ({ hook, count }))
|
|
1695
|
+
};
|
|
1696
|
+
}
|
|
1697
|
+
reset() {
|
|
1698
|
+
this.executions.clear();
|
|
1699
|
+
this.skips.clear();
|
|
1700
|
+
this.retries.clear();
|
|
1701
|
+
}
|
|
1702
|
+
};
|
|
1703
|
+
|
|
1704
|
+
// src/hook-wrappers.ts
|
|
1705
|
+
var noopLogger = {
|
|
1706
|
+
debug: () => {
|
|
1707
|
+
},
|
|
1708
|
+
info: () => {
|
|
1709
|
+
},
|
|
1710
|
+
warn: () => {
|
|
1711
|
+
},
|
|
1712
|
+
error: () => {
|
|
1713
|
+
}
|
|
1714
|
+
};
|
|
1715
|
+
function wrapDeclarativeHook(meta, handler, opts = {}) {
|
|
1716
|
+
const logger = opts.logger ?? noopLogger;
|
|
1717
|
+
const metrics = opts.metrics ?? noopHookMetricsRecorder;
|
|
1718
|
+
const isAfterEvent = meta.events?.some((e) => typeof e === "string" && e.startsWith("after")) ?? false;
|
|
1719
|
+
const hasBody = Boolean(meta.body);
|
|
1720
|
+
const labelFor = (ctx) => ({
|
|
1721
|
+
hook: meta.name,
|
|
1722
|
+
object: ctx.object ?? (typeof meta.object === "string" ? meta.object : void 0),
|
|
1723
|
+
event: ctx.event,
|
|
1724
|
+
body: hasBody
|
|
1725
|
+
});
|
|
1726
|
+
let conditionFn;
|
|
1727
|
+
if (meta.condition) {
|
|
1728
|
+
const expr = typeof meta.condition === "string" ? { dialect: "cel", source: meta.condition } : meta.condition;
|
|
1729
|
+
if (expr.source && expr.source.trim()) {
|
|
1730
|
+
const check = ExpressionEngine.compile(expr);
|
|
1731
|
+
if (check.ok) {
|
|
1732
|
+
conditionFn = (record) => {
|
|
1733
|
+
const r = ExpressionEngine.evaluate(expr, { record: record ?? {} });
|
|
1734
|
+
if (!r.ok) {
|
|
1735
|
+
logger.warn("[hook] condition evaluation failed; treating as false", {
|
|
1736
|
+
hook: meta.name,
|
|
1737
|
+
condition: expr.source,
|
|
1738
|
+
error: r.error.message
|
|
1739
|
+
});
|
|
1740
|
+
return false;
|
|
1741
|
+
}
|
|
1742
|
+
return Boolean(r.value);
|
|
1743
|
+
};
|
|
1744
|
+
} else {
|
|
1745
|
+
logger.warn("[hook] condition formula failed to compile; condition ignored", {
|
|
1746
|
+
hook: meta.name,
|
|
1747
|
+
condition: expr.source,
|
|
1748
|
+
error: check.error.message
|
|
1749
|
+
});
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
const retryMax = Math.max(0, Number(meta.retryPolicy?.maxRetries ?? 0));
|
|
1754
|
+
const retryBackoffMs = Math.max(0, Number(meta.retryPolicy?.backoffMs ?? 0));
|
|
1755
|
+
const timeoutMs = typeof meta.timeout === "number" && meta.timeout > 0 ? meta.timeout : void 0;
|
|
1756
|
+
const onError = meta.onError ?? "abort";
|
|
1757
|
+
const fireAndForget = Boolean(meta.async) && isAfterEvent;
|
|
1758
|
+
const runWithTimeout = async (ctx) => {
|
|
1759
|
+
if (!timeoutMs) {
|
|
1760
|
+
await handler(ctx);
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
let timer;
|
|
1764
|
+
try {
|
|
1765
|
+
await Promise.race([
|
|
1766
|
+
Promise.resolve().then(() => handler(ctx)),
|
|
1767
|
+
new Promise((_, reject) => {
|
|
1768
|
+
timer = setTimeout(() => {
|
|
1769
|
+
reject(new Error(`Hook '${meta.name}' timed out after ${timeoutMs}ms`));
|
|
1770
|
+
}, timeoutMs);
|
|
1771
|
+
})
|
|
1772
|
+
]);
|
|
1773
|
+
} finally {
|
|
1774
|
+
if (timer) clearTimeout(timer);
|
|
1775
|
+
}
|
|
1776
|
+
};
|
|
1777
|
+
const runWithRetry = async (ctx) => {
|
|
1778
|
+
let attempt = 0;
|
|
1779
|
+
let lastErr;
|
|
1780
|
+
while (attempt <= retryMax) {
|
|
1781
|
+
try {
|
|
1782
|
+
await runWithTimeout(ctx);
|
|
1783
|
+
return;
|
|
1784
|
+
} catch (err) {
|
|
1785
|
+
lastErr = err;
|
|
1786
|
+
attempt += 1;
|
|
1787
|
+
if (attempt > retryMax) break;
|
|
1788
|
+
if (retryBackoffMs > 0) {
|
|
1789
|
+
await new Promise((r) => setTimeout(r, retryBackoffMs * attempt));
|
|
1790
|
+
}
|
|
1791
|
+
try {
|
|
1792
|
+
metrics.recordRetry(labelFor(ctx), attempt);
|
|
1793
|
+
} catch {
|
|
1794
|
+
}
|
|
1795
|
+
logger.warn("[hook] retrying after failure", {
|
|
1796
|
+
hook: meta.name,
|
|
1797
|
+
attempt,
|
|
1798
|
+
maxRetries: retryMax,
|
|
1799
|
+
error: err?.message
|
|
1800
|
+
});
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
throw lastErr;
|
|
1804
|
+
};
|
|
1805
|
+
const runWithErrorPolicy = async (ctx) => {
|
|
1806
|
+
try {
|
|
1807
|
+
await runWithRetry(ctx);
|
|
1808
|
+
} catch (err) {
|
|
1809
|
+
if (onError === "log") {
|
|
1810
|
+
logger.error("[hook] handler failed (onError=log; suppressing)", {
|
|
1811
|
+
hook: meta.name,
|
|
1812
|
+
object: ctx.object,
|
|
1813
|
+
event: ctx.event,
|
|
1814
|
+
error: err?.message
|
|
1815
|
+
});
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
throw err;
|
|
1819
|
+
}
|
|
1820
|
+
};
|
|
1821
|
+
return async (ctx) => {
|
|
1822
|
+
if (conditionFn) {
|
|
1823
|
+
const record = pickRecordPayload(ctx);
|
|
1824
|
+
if (!conditionFn(record)) {
|
|
1825
|
+
logger.debug("[hook] skipped by condition", {
|
|
1826
|
+
hook: meta.name,
|
|
1827
|
+
object: ctx.object,
|
|
1828
|
+
event: ctx.event
|
|
1829
|
+
});
|
|
1830
|
+
try {
|
|
1831
|
+
metrics.recordSkip(labelFor(ctx), "condition");
|
|
1832
|
+
} catch {
|
|
1833
|
+
}
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
const restore = installFlatInput(ctx);
|
|
1838
|
+
const startedAt = Date.now();
|
|
1839
|
+
const recordOutcome = (err) => {
|
|
1840
|
+
const elapsed = Date.now() - startedAt;
|
|
1841
|
+
let outcome = "success";
|
|
1842
|
+
if (err) {
|
|
1843
|
+
const msg = String(err?.message ?? err ?? "");
|
|
1844
|
+
if (/timed out after/i.test(msg)) outcome = "timeout";
|
|
1845
|
+
else if (/capability|cap-rejection|capability_rejected/i.test(msg)) outcome = "capability_rejected";
|
|
1846
|
+
else outcome = "error";
|
|
1847
|
+
}
|
|
1848
|
+
try {
|
|
1849
|
+
metrics.recordExecution(labelFor(ctx), outcome, elapsed);
|
|
1850
|
+
} catch {
|
|
1851
|
+
}
|
|
1852
|
+
};
|
|
1853
|
+
try {
|
|
1854
|
+
if (fireAndForget) {
|
|
1855
|
+
try {
|
|
1856
|
+
metrics.recordSkip(labelFor(ctx), "fire_and_forget");
|
|
1857
|
+
} catch {
|
|
1858
|
+
}
|
|
1859
|
+
void runWithErrorPolicy(ctx).then(() => recordOutcome()).catch((err) => {
|
|
1860
|
+
recordOutcome(err);
|
|
1861
|
+
logger.error("[hook] async handler error (fire-and-forget)", {
|
|
1862
|
+
hook: meta.name,
|
|
1863
|
+
error: err?.message
|
|
1864
|
+
});
|
|
1865
|
+
});
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
try {
|
|
1869
|
+
await runWithErrorPolicy(ctx);
|
|
1870
|
+
recordOutcome();
|
|
1871
|
+
} catch (err) {
|
|
1872
|
+
recordOutcome(err);
|
|
1873
|
+
throw err;
|
|
1874
|
+
}
|
|
1875
|
+
} finally {
|
|
1876
|
+
restore();
|
|
1877
|
+
}
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
function installFlatInput(ctx) {
|
|
1881
|
+
const raw = ctx.input ?? {};
|
|
1882
|
+
const looksWrapped = raw && typeof raw === "object" && ("data" in raw || "options" in raw || "id" in raw || "ast" in raw);
|
|
1883
|
+
if (!looksWrapped) return () => {
|
|
1884
|
+
};
|
|
1885
|
+
const ensureData = () => {
|
|
1886
|
+
if (!raw.data || typeof raw.data !== "object") {
|
|
1887
|
+
raw.data = {};
|
|
1888
|
+
}
|
|
1889
|
+
return raw.data;
|
|
1890
|
+
};
|
|
1891
|
+
const proxy = new Proxy(raw, {
|
|
1892
|
+
get(target, prop, receiver) {
|
|
1893
|
+
if (prop === "id" || prop === "options" || prop === "ast" || prop === "data") {
|
|
1894
|
+
return Reflect.get(target, prop, receiver);
|
|
1895
|
+
}
|
|
1896
|
+
const data = target.data;
|
|
1897
|
+
if (data && typeof data === "object" && prop in data) {
|
|
1898
|
+
return data[prop];
|
|
1899
|
+
}
|
|
1900
|
+
return Reflect.get(target, prop, receiver);
|
|
1901
|
+
},
|
|
1902
|
+
set(target, prop, value) {
|
|
1903
|
+
if (prop === "id" || prop === "options" || prop === "ast" || prop === "data") {
|
|
1904
|
+
target[prop] = value;
|
|
1905
|
+
return true;
|
|
1906
|
+
}
|
|
1907
|
+
ensureData()[prop] = value;
|
|
1908
|
+
return true;
|
|
1909
|
+
},
|
|
1910
|
+
has(target, prop) {
|
|
1911
|
+
if (prop === "id" || prop === "options" || prop === "ast" || prop === "data") {
|
|
1912
|
+
return prop in target;
|
|
1913
|
+
}
|
|
1914
|
+
const data = target.data;
|
|
1915
|
+
if (data && typeof data === "object" && prop in data) return true;
|
|
1916
|
+
return prop in target;
|
|
1917
|
+
},
|
|
1918
|
+
ownKeys(target) {
|
|
1919
|
+
const dataKeys = target.data && typeof target.data === "object" ? Object.keys(target.data) : [];
|
|
1920
|
+
return Array.from(new Set(dataKeys));
|
|
1921
|
+
},
|
|
1922
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
1923
|
+
const data = target.data;
|
|
1924
|
+
if (data && typeof data === "object" && prop in data) {
|
|
1925
|
+
return { configurable: true, enumerable: true, writable: true, value: data[prop] };
|
|
1926
|
+
}
|
|
1927
|
+
if (prop === "id" || prop === "options" || prop === "ast" || prop === "data") {
|
|
1928
|
+
const desc = Object.getOwnPropertyDescriptor(target, prop);
|
|
1929
|
+
return desc ? { ...desc, enumerable: false } : void 0;
|
|
1930
|
+
}
|
|
1931
|
+
return Object.getOwnPropertyDescriptor(target, prop);
|
|
1932
|
+
}
|
|
1933
|
+
});
|
|
1934
|
+
ctx.input = proxy;
|
|
1935
|
+
return () => {
|
|
1936
|
+
ctx.input = raw;
|
|
1937
|
+
};
|
|
1938
|
+
}
|
|
1939
|
+
function pickRecordPayload(ctx) {
|
|
1940
|
+
const input = ctx.input ?? {};
|
|
1941
|
+
if (input && typeof input === "object" && input.data && typeof input.data === "object") {
|
|
1942
|
+
return input.data;
|
|
1943
|
+
}
|
|
1944
|
+
if (ctx.previous && typeof ctx.previous === "object") {
|
|
1945
|
+
return ctx.previous;
|
|
1946
|
+
}
|
|
1947
|
+
return input;
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
// src/hook-binder.ts
|
|
1951
|
+
var noopLogger2 = {
|
|
1952
|
+
debug: () => {
|
|
1953
|
+
},
|
|
1954
|
+
info: () => {
|
|
1955
|
+
},
|
|
1956
|
+
warn: () => {
|
|
1957
|
+
},
|
|
1958
|
+
error: () => {
|
|
1959
|
+
}
|
|
1960
|
+
};
|
|
1961
|
+
function bindHooksToEngine(engine, hooks, opts = {}) {
|
|
1962
|
+
const logger = opts.logger ?? noopLogger2;
|
|
1963
|
+
const result = { registered: 0, skipped: 0, errors: [] };
|
|
1964
|
+
if (!Array.isArray(hooks) || hooks.length === 0) {
|
|
1965
|
+
return result;
|
|
1966
|
+
}
|
|
1967
|
+
if (opts.packageId && typeof engine.unregisterHooksByPackage === "function") {
|
|
1968
|
+
try {
|
|
1969
|
+
engine.unregisterHooksByPackage(opts.packageId);
|
|
1970
|
+
} catch (err) {
|
|
1971
|
+
logger.warn("[hook-binder] unregister-by-package failed; continuing", {
|
|
1972
|
+
packageId: opts.packageId,
|
|
1973
|
+
error: err?.message
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
if (opts.functions && typeof engine.registerFunction === "function") {
|
|
1978
|
+
for (const [name, fn] of Object.entries(opts.functions)) {
|
|
1979
|
+
try {
|
|
1980
|
+
engine.registerFunction(name, fn, opts.packageId);
|
|
1981
|
+
} catch (err) {
|
|
1982
|
+
logger.warn("[hook-binder] failed to register function", {
|
|
1983
|
+
name,
|
|
1984
|
+
error: err?.message
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
for (const hook of hooks) {
|
|
1990
|
+
try {
|
|
1991
|
+
const resolved = resolveHandler(engine, hook, opts);
|
|
1992
|
+
if (!resolved) {
|
|
1993
|
+
result.skipped += 1;
|
|
1994
|
+
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";
|
|
1995
|
+
result.errors.push({ hook: hook.name, reason });
|
|
1996
|
+
if (opts.strict) {
|
|
1997
|
+
throw new Error(`[hook-binder] strict: cannot bind hook '${hook.name}': ${reason}`);
|
|
1998
|
+
}
|
|
1999
|
+
logger.warn("[hook-binder] skipping hook with unresolved handler", {
|
|
2000
|
+
hook: hook.name,
|
|
2001
|
+
handler: hook.handler,
|
|
2002
|
+
hasBody: Boolean(hook.body)
|
|
2003
|
+
});
|
|
2004
|
+
continue;
|
|
2005
|
+
}
|
|
2006
|
+
if (opts.warnLegacyHandler && !hook.body && typeof hook.handler === "string") {
|
|
2007
|
+
logger.warn("[hook-binder] DEPRECATED: hook uses legacy handler ref without body", {
|
|
2008
|
+
hook: hook.name,
|
|
2009
|
+
handler: hook.handler,
|
|
2010
|
+
hint: "Move the handler source into Hook.body so the artifact stays metadata-only and the .mjs runtime bundle can be dropped."
|
|
2011
|
+
});
|
|
2012
|
+
}
|
|
2013
|
+
const wrapped = wrapDeclarativeHook(hook, resolved, { logger, metrics: opts.metrics });
|
|
2014
|
+
const objects = normalizeObjects(hook.object);
|
|
2015
|
+
const events = Array.isArray(hook.events) ? hook.events : [];
|
|
2016
|
+
for (const event of events) {
|
|
2017
|
+
for (const object of objects) {
|
|
2018
|
+
engine.registerHook(event, wrapped, {
|
|
2019
|
+
object,
|
|
2020
|
+
priority: typeof hook.priority === "number" ? hook.priority : 100,
|
|
2021
|
+
packageId: opts.packageId,
|
|
2022
|
+
// Reflect metadata so future tooling can introspect / unregister
|
|
2023
|
+
// and so we can detect duplicate name collisions.
|
|
2024
|
+
// The engine ignores unknown options today; this is forward-only.
|
|
2025
|
+
...{ meta: hook, hookName: hook.name }
|
|
2026
|
+
});
|
|
2027
|
+
result.registered += 1;
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
} catch (err) {
|
|
2031
|
+
result.errors.push({ hook: hook.name, reason: err?.message ?? String(err) });
|
|
2032
|
+
logger.error("[hook-binder] failed to bind hook", {
|
|
2033
|
+
hook: hook.name,
|
|
2034
|
+
error: err?.message
|
|
2035
|
+
});
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
if (result.registered > 0) {
|
|
2039
|
+
logger.debug("[hook-binder] hooks bound", {
|
|
2040
|
+
packageId: opts.packageId,
|
|
2041
|
+
registered: result.registered,
|
|
2042
|
+
skipped: result.skipped
|
|
2043
|
+
});
|
|
2044
|
+
}
|
|
2045
|
+
return result;
|
|
2046
|
+
}
|
|
2047
|
+
function normalizeObjects(target) {
|
|
2048
|
+
if (Array.isArray(target)) return target.length > 0 ? target : ["*"];
|
|
2049
|
+
if (typeof target === "string" && target.length > 0) return [target];
|
|
2050
|
+
return ["*"];
|
|
2051
|
+
}
|
|
2052
|
+
function resolveHandler(engine, hook, opts) {
|
|
2053
|
+
const body = hook.body;
|
|
2054
|
+
if (body && typeof body === "object") {
|
|
2055
|
+
let runner = opts.bodyRunner;
|
|
2056
|
+
if (typeof runner !== "function") {
|
|
2057
|
+
const fallback = engine?._defaultBodyRunner;
|
|
2058
|
+
if (typeof fallback === "function") runner = fallback;
|
|
2059
|
+
}
|
|
2060
|
+
if (typeof runner !== "function") {
|
|
2061
|
+
return void 0;
|
|
2062
|
+
}
|
|
2063
|
+
const fn = runner(hook);
|
|
2064
|
+
if (typeof fn === "function") return fn;
|
|
2065
|
+
return void 0;
|
|
2066
|
+
}
|
|
2067
|
+
const h = hook.handler;
|
|
2068
|
+
if (typeof h === "function") return h;
|
|
2069
|
+
if (typeof h === "string" && h.length > 0) {
|
|
2070
|
+
const fromBundle = opts.functions?.[h];
|
|
2071
|
+
if (typeof fromBundle === "function") return fromBundle;
|
|
2072
|
+
if (typeof engine.resolveFunction === "function") {
|
|
2073
|
+
const fn = engine.resolveFunction(h);
|
|
2074
|
+
if (typeof fn === "function") return fn;
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
return void 0;
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
// src/engine.ts
|
|
2081
|
+
function planFormulaProjection(schema, requestedFields) {
|
|
2082
|
+
if (!schema?.fields) return { plan: [] };
|
|
2083
|
+
const allFieldNames = Object.keys(schema.fields);
|
|
2084
|
+
const targets = Array.isArray(requestedFields) && requestedFields.length > 0 ? requestedFields : allFieldNames;
|
|
2085
|
+
const plan = [];
|
|
2086
|
+
const projected = /* @__PURE__ */ new Set();
|
|
2087
|
+
for (const f of targets) {
|
|
2088
|
+
const def = schema.fields[f];
|
|
2089
|
+
if (def?.type === "formula" && def.expression) {
|
|
2090
|
+
const expr = typeof def.expression === "string" ? { dialect: "cel", source: def.expression } : def.expression;
|
|
2091
|
+
plan.push({ name: f, expression: expr });
|
|
2092
|
+
ExpressionEngine2.compile(expr);
|
|
2093
|
+
} else if (Array.isArray(requestedFields) && requestedFields.length > 0) {
|
|
2094
|
+
projected.add(f);
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
if (plan.length === 0) return { plan: [] };
|
|
2098
|
+
if (Array.isArray(requestedFields) && requestedFields.length > 0) {
|
|
2099
|
+
if (!projected.has("id")) projected.add("id");
|
|
2100
|
+
for (const fname of allFieldNames) projected.add(fname);
|
|
2101
|
+
return { plan, projected: Array.from(projected) };
|
|
2102
|
+
}
|
|
2103
|
+
return { plan };
|
|
2104
|
+
}
|
|
2105
|
+
function applyFormulaPlan(plan, records) {
|
|
2106
|
+
if (!plan.length) return;
|
|
2107
|
+
for (const rec of records) {
|
|
2108
|
+
if (rec == null) continue;
|
|
2109
|
+
for (const fp of plan) {
|
|
2110
|
+
const r = ExpressionEngine2.evaluate(fp.expression, { record: rec });
|
|
2111
|
+
rec[fp.name] = r.ok ? r.value : null;
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
function resolveMetadataItemName(key, item) {
|
|
2116
|
+
if (!item) return void 0;
|
|
2117
|
+
if (item.name) return item.name;
|
|
2118
|
+
if (item.id) return item.id;
|
|
2119
|
+
if (key === "views") {
|
|
2120
|
+
return item?.list?.data?.object || item?.form?.data?.object || void 0;
|
|
2121
|
+
}
|
|
2122
|
+
return void 0;
|
|
2123
|
+
}
|
|
1514
2124
|
var _ObjectQL = class _ObjectQL {
|
|
1515
2125
|
constructor(hostContext = {}) {
|
|
1516
2126
|
this.drivers = /* @__PURE__ */ new Map();
|
|
1517
2127
|
this.defaultDriver = null;
|
|
2128
|
+
// Datasource mapping rules (imported from defineStack)
|
|
2129
|
+
this.datasourceMapping = [];
|
|
2130
|
+
// Package manifests registry (for defaultDatasource lookup)
|
|
2131
|
+
this.manifests = /* @__PURE__ */ new Map();
|
|
1518
2132
|
// Per-object hooks with priority support
|
|
1519
2133
|
this.hooks = /* @__PURE__ */ new Map([
|
|
1520
2134
|
["beforeFind", []],
|
|
@@ -1530,10 +2144,29 @@ var _ObjectQL = class _ObjectQL {
|
|
|
1530
2144
|
this.middlewares = [];
|
|
1531
2145
|
// Action registry: key = "objectName:actionName"
|
|
1532
2146
|
this.actions = /* @__PURE__ */ new Map();
|
|
2147
|
+
// Function registry: name → handler. Used by `bindHooksToEngine` to
|
|
2148
|
+
// resolve string-named hook handlers (the JSON-safe form). Populated by
|
|
2149
|
+
// `defineStack({ functions })` via `AppPlugin`, or directly via
|
|
2150
|
+
// `engine.registerFunction(...)`.
|
|
2151
|
+
this.functions = /* @__PURE__ */ new Map();
|
|
1533
2152
|
// Host provided context additions (e.g. Server router)
|
|
1534
2153
|
this.hostContext = {};
|
|
2154
|
+
// Per-engine SchemaRegistry instance.
|
|
2155
|
+
//
|
|
2156
|
+
// Historically SchemaRegistry was a process-wide singleton of static state,
|
|
2157
|
+
// which broke multi-project servers: a project kernel would inherit every
|
|
2158
|
+
// object registered by the control plane (e.g. sys_metadata), and
|
|
2159
|
+
// getDriver()'s owner lookup would route CRUD to the wrong database. Each
|
|
2160
|
+
// engine now owns its registry so kernels are fully isolated.
|
|
2161
|
+
this._registry = new SchemaRegistry();
|
|
1535
2162
|
this.hostContext = hostContext;
|
|
1536
2163
|
this.logger = hostContext.logger || createLogger({ level: "info", format: "pretty" });
|
|
2164
|
+
if (process?.env?.OBJECTQL_STRICT_HOOKS === "1") {
|
|
2165
|
+
this._strictHookBinding = true;
|
|
2166
|
+
}
|
|
2167
|
+
if (process?.env?.OBJECTQL_WARN_LEGACY_HANDLER === "1") {
|
|
2168
|
+
this._warnLegacyHandler = true;
|
|
2169
|
+
}
|
|
1537
2170
|
this.logger.info("ObjectQL Engine Instance Created");
|
|
1538
2171
|
}
|
|
1539
2172
|
/**
|
|
@@ -1549,10 +2182,13 @@ var _ObjectQL = class _ObjectQL {
|
|
|
1549
2182
|
};
|
|
1550
2183
|
}
|
|
1551
2184
|
/**
|
|
1552
|
-
* Expose the SchemaRegistry for plugins to register metadata
|
|
2185
|
+
* Expose the SchemaRegistry for plugins to register metadata.
|
|
2186
|
+
*
|
|
2187
|
+
* Returns the per-engine instance, NOT the class. Each ObjectQL engine
|
|
2188
|
+
* owns its registry so multi-project kernels remain isolated.
|
|
1553
2189
|
*/
|
|
1554
2190
|
get registry() {
|
|
1555
|
-
return
|
|
2191
|
+
return this._registry;
|
|
1556
2192
|
}
|
|
1557
2193
|
/**
|
|
1558
2194
|
* Load and Register a Plugin
|
|
@@ -1598,11 +2234,121 @@ var _ObjectQL = class _ObjectQL {
|
|
|
1598
2234
|
handler,
|
|
1599
2235
|
object: options?.object,
|
|
1600
2236
|
priority: options?.priority ?? 100,
|
|
1601
|
-
packageId: options?.packageId
|
|
2237
|
+
packageId: options?.packageId,
|
|
2238
|
+
meta: options?.meta,
|
|
2239
|
+
hookName: options?.hookName
|
|
1602
2240
|
});
|
|
1603
2241
|
entries.sort((a, b) => a.priority - b.priority);
|
|
1604
2242
|
this.logger.debug("Registered hook", { event, object: options?.object, priority: options?.priority ?? 100, totalHandlers: entries.length });
|
|
1605
2243
|
}
|
|
2244
|
+
/**
|
|
2245
|
+
* Remove all hooks registered under a given `packageId`. Used by
|
|
2246
|
+
* `bindHooksToEngine` to make re-binding (hot reload, app reinstall)
|
|
2247
|
+
* idempotent, and by app uninstall flows.
|
|
2248
|
+
*/
|
|
2249
|
+
unregisterHooksByPackage(packageId) {
|
|
2250
|
+
if (!packageId) return 0;
|
|
2251
|
+
let removed = 0;
|
|
2252
|
+
for (const [event, entries] of this.hooks.entries()) {
|
|
2253
|
+
const before = entries.length;
|
|
2254
|
+
const kept = entries.filter((e) => e.packageId !== packageId);
|
|
2255
|
+
if (kept.length !== before) {
|
|
2256
|
+
this.hooks.set(event, kept);
|
|
2257
|
+
removed += before - kept.length;
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
if (removed > 0) {
|
|
2261
|
+
this.logger.debug("Unregistered hooks by package", { packageId, removed });
|
|
2262
|
+
}
|
|
2263
|
+
return removed;
|
|
2264
|
+
}
|
|
2265
|
+
/**
|
|
2266
|
+
* Register a named function handler that can later be referenced by
|
|
2267
|
+
* string from a `Hook.handler` field. This is the JSON-safe form of
|
|
2268
|
+
* handler binding — declarative metadata persisted to disk or shipped
|
|
2269
|
+
* over the wire only carries the name.
|
|
2270
|
+
*/
|
|
2271
|
+
registerFunction(name, handler, packageId) {
|
|
2272
|
+
if (!name || typeof handler !== "function") return;
|
|
2273
|
+
this.functions.set(name, { handler, packageId });
|
|
2274
|
+
this.logger.debug("Registered function", { name, packageId });
|
|
2275
|
+
}
|
|
2276
|
+
/** Look up a registered function by name. */
|
|
2277
|
+
resolveFunction(name) {
|
|
2278
|
+
return this.functions.get(name)?.handler;
|
|
2279
|
+
}
|
|
2280
|
+
/** Remove all functions registered under a given `packageId`. */
|
|
2281
|
+
unregisterFunctionsByPackage(packageId) {
|
|
2282
|
+
if (!packageId) return 0;
|
|
2283
|
+
let removed = 0;
|
|
2284
|
+
for (const [name, entry] of this.functions.entries()) {
|
|
2285
|
+
if (entry.packageId === packageId) {
|
|
2286
|
+
this.functions.delete(name);
|
|
2287
|
+
removed += 1;
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
if (removed > 0) {
|
|
2291
|
+
this.logger.debug("Unregistered functions by package", { packageId, removed });
|
|
2292
|
+
}
|
|
2293
|
+
return removed;
|
|
2294
|
+
}
|
|
2295
|
+
/**
|
|
2296
|
+
* Bind a list of declarative `Hook` metadata definitions to this engine.
|
|
2297
|
+
*
|
|
2298
|
+
* Convenience proxy to the canonical `bindHooksToEngine` so callers do
|
|
2299
|
+
* not need a separate import. Use `import { bindHooksToEngine } from
|
|
2300
|
+
* '@objectstack/objectql'` directly when you want the result object.
|
|
2301
|
+
*/
|
|
2302
|
+
bindHooks(hooks, opts) {
|
|
2303
|
+
const merged = { ...opts ?? {}, logger: this.logger };
|
|
2304
|
+
if (!merged.bodyRunner && this._defaultBodyRunner) {
|
|
2305
|
+
merged.bodyRunner = this._defaultBodyRunner;
|
|
2306
|
+
}
|
|
2307
|
+
if (merged.strict === void 0 && this._strictHookBinding) {
|
|
2308
|
+
merged.strict = true;
|
|
2309
|
+
}
|
|
2310
|
+
if (merged.warnLegacyHandler === void 0 && this._warnLegacyHandler) {
|
|
2311
|
+
merged.warnLegacyHandler = true;
|
|
2312
|
+
}
|
|
2313
|
+
if (!merged.metrics && this._hookMetricsRecorder) {
|
|
2314
|
+
merged.metrics = this._hookMetricsRecorder;
|
|
2315
|
+
}
|
|
2316
|
+
bindHooksToEngine(this, hooks, merged);
|
|
2317
|
+
}
|
|
2318
|
+
/**
|
|
2319
|
+
* Install a default body-runner used when `bindHooks` is called without
|
|
2320
|
+
* an explicit one. The runtime layer sets this once on each per-project
|
|
2321
|
+
* engine so every binding path (template seed, metadata sync, AppPlugin)
|
|
2322
|
+
* can execute hook `body.source` consistently.
|
|
2323
|
+
*/
|
|
2324
|
+
setDefaultBodyRunner(runner) {
|
|
2325
|
+
this._defaultBodyRunner = runner;
|
|
2326
|
+
}
|
|
2327
|
+
/**
|
|
2328
|
+
* Toggle strict hook-binding mode for this engine. When enabled, every
|
|
2329
|
+
* subsequent `bindHooks` call rejects on the first unresolved hook
|
|
2330
|
+
* instead of silently warning. Production runtimes should enable this.
|
|
2331
|
+
*/
|
|
2332
|
+
setStrictHookBinding(strict) {
|
|
2333
|
+
this._strictHookBinding = strict;
|
|
2334
|
+
}
|
|
2335
|
+
/** Toggle deprecation warnings for hooks still using legacy `handler` ref. */
|
|
2336
|
+
setWarnLegacyHandler(warn) {
|
|
2337
|
+
this._warnLegacyHandler = warn;
|
|
2338
|
+
}
|
|
2339
|
+
/**
|
|
2340
|
+
* Install a metrics recorder used by every subsequent `bindHooks` call.
|
|
2341
|
+
* The recorder's methods are invoked per-execution to count outcomes
|
|
2342
|
+
* (success / error / timeout / capability_rejected), skips, and retries.
|
|
2343
|
+
* Defaults to no-op so the engine pays zero cost when nobody is observing.
|
|
2344
|
+
*/
|
|
2345
|
+
setHookMetricsRecorder(recorder) {
|
|
2346
|
+
this._hookMetricsRecorder = recorder;
|
|
2347
|
+
}
|
|
2348
|
+
/** Read the engine's installed metrics recorder, if any. */
|
|
2349
|
+
getHookMetricsRecorder() {
|
|
2350
|
+
return this._hookMetricsRecorder;
|
|
2351
|
+
}
|
|
1606
2352
|
async triggerHooks(event, context) {
|
|
1607
2353
|
const entries = this.hooks.get(event) || [];
|
|
1608
2354
|
if (entries.length === 0) {
|
|
@@ -1698,6 +2444,62 @@ var _ObjectQL = class _ObjectQL {
|
|
|
1698
2444
|
accessToken: execCtx.accessToken
|
|
1699
2445
|
};
|
|
1700
2446
|
}
|
|
2447
|
+
/**
|
|
2448
|
+
* Build a HookContext.api: a ScopedContext that hooks can use to
|
|
2449
|
+
* read/write other objects within the same execution context.
|
|
2450
|
+
* Falls back to a system-elevated empty context when no execCtx
|
|
2451
|
+
* is supplied (e.g. system-triggered hooks).
|
|
2452
|
+
*/
|
|
2453
|
+
buildHookApi(execCtx) {
|
|
2454
|
+
const safeCtx = execCtx ?? { isSystem: true };
|
|
2455
|
+
return new ScopedContext(safeCtx, this);
|
|
2456
|
+
}
|
|
2457
|
+
/**
|
|
2458
|
+
* Apply field defaults to an incoming insert payload. Defaults that are
|
|
2459
|
+
* Expression envelopes (e.g. `{ dialect: 'cel', source: 'today()' }`,
|
|
2460
|
+
* `{ dialect: 'cel', source: 'os.user.id' }`) are evaluated via
|
|
2461
|
+
* `ExpressionEngine` against the calling user/org/now snapshot. Static
|
|
2462
|
+
* defaults are applied verbatim. Records that already supplied a value for a
|
|
2463
|
+
* field are left untouched.
|
|
2464
|
+
*
|
|
2465
|
+
* Implements ROADMAP §M9.9b — `defaultValue` accepts Expression so authors
|
|
2466
|
+
* can replace "write a hook to default to today/current-user" with a
|
|
2467
|
+
* declarative `defaultValue: cel\`today()\``.
|
|
2468
|
+
*/
|
|
2469
|
+
applyFieldDefaults(object, record, execCtx, nowSnapshot) {
|
|
2470
|
+
const schema = this.getSchema(object);
|
|
2471
|
+
const fieldsRaw = schema?.fields;
|
|
2472
|
+
if (!fieldsRaw || typeof fieldsRaw !== "object") return record;
|
|
2473
|
+
const fieldEntries = Array.isArray(fieldsRaw) ? fieldsRaw : Object.entries(fieldsRaw).map(([name, def]) => ({ name, ...def }));
|
|
2474
|
+
const out = { ...record };
|
|
2475
|
+
const now = nowSnapshot ?? /* @__PURE__ */ new Date();
|
|
2476
|
+
for (const f of fieldEntries) {
|
|
2477
|
+
if (out[f.name] !== void 0) continue;
|
|
2478
|
+
if (f.defaultValue == null) continue;
|
|
2479
|
+
const dv = f.defaultValue;
|
|
2480
|
+
if (typeof dv === "object" && dv !== null && dv.dialect && typeof dv.source === "string") {
|
|
2481
|
+
const result = ExpressionEngine2.evaluate(dv, {
|
|
2482
|
+
now,
|
|
2483
|
+
user: execCtx?.userId ? { id: String(execCtx.userId), role: execCtx?.roles?.[0] } : void 0,
|
|
2484
|
+
org: execCtx?.tenantId ? { id: String(execCtx.tenantId) } : void 0,
|
|
2485
|
+
record: out,
|
|
2486
|
+
extra: { object }
|
|
2487
|
+
});
|
|
2488
|
+
if (result.ok) {
|
|
2489
|
+
out[f.name] = result.value;
|
|
2490
|
+
} else {
|
|
2491
|
+
this.logger.warn("Failed to evaluate default expression", {
|
|
2492
|
+
object,
|
|
2493
|
+
field: f.name,
|
|
2494
|
+
error: result.error
|
|
2495
|
+
});
|
|
2496
|
+
}
|
|
2497
|
+
} else {
|
|
2498
|
+
out[f.name] = dv;
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
return out;
|
|
2502
|
+
}
|
|
1701
2503
|
/**
|
|
1702
2504
|
* Register contribution (Manifest)
|
|
1703
2505
|
*
|
|
@@ -1712,20 +2514,24 @@ var _ObjectQL = class _ObjectQL {
|
|
|
1712
2514
|
const id = manifest.id || manifest.name;
|
|
1713
2515
|
const namespace = manifest.namespace;
|
|
1714
2516
|
this.logger.debug("Registering package manifest", { id, namespace });
|
|
1715
|
-
|
|
2517
|
+
console.warn(`[ObjectQL:registerApp] id=${id} flows=${Array.isArray(manifest.flows) ? manifest.flows.length : typeof manifest.flows} keys=${Object.keys(manifest).join(",")}`);
|
|
2518
|
+
if (id) {
|
|
2519
|
+
this.manifests.set(id, manifest);
|
|
2520
|
+
}
|
|
2521
|
+
this._registry.installPackage(manifest);
|
|
1716
2522
|
this.logger.debug("Installed Package", { id: manifest.id, name: manifest.name, namespace });
|
|
1717
2523
|
if (manifest.objects) {
|
|
1718
2524
|
if (Array.isArray(manifest.objects)) {
|
|
1719
2525
|
this.logger.debug("Registering objects from manifest (Array)", { id, objectCount: manifest.objects.length });
|
|
1720
2526
|
for (const objDef of manifest.objects) {
|
|
1721
|
-
const fqn =
|
|
2527
|
+
const fqn = this._registry.registerObject(objDef, id, namespace, "own");
|
|
1722
2528
|
this.logger.debug("Registered Object", { fqn, from: id });
|
|
1723
2529
|
}
|
|
1724
2530
|
} else {
|
|
1725
2531
|
this.logger.debug("Registering objects from manifest (Map)", { id, objectCount: Object.keys(manifest.objects).length });
|
|
1726
2532
|
for (const [name, objDef] of Object.entries(manifest.objects)) {
|
|
1727
2533
|
objDef.name = name;
|
|
1728
|
-
const fqn =
|
|
2534
|
+
const fqn = this._registry.registerObject(objDef, id, namespace, "own");
|
|
1729
2535
|
this.logger.debug("Registered Object", { fqn, from: id });
|
|
1730
2536
|
}
|
|
1731
2537
|
}
|
|
@@ -1745,7 +2551,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
1745
2551
|
validations: ext.validations,
|
|
1746
2552
|
indexes: ext.indexes
|
|
1747
2553
|
};
|
|
1748
|
-
|
|
2554
|
+
this._registry.registerObject(extDef, id, void 0, "extend", priority);
|
|
1749
2555
|
this.logger.debug("Registered Object Extension", { target: targetFqn, priority, from: id });
|
|
1750
2556
|
}
|
|
1751
2557
|
}
|
|
@@ -1754,13 +2560,15 @@ var _ObjectQL = class _ObjectQL {
|
|
|
1754
2560
|
for (const app of manifest.apps) {
|
|
1755
2561
|
const appName = app.name || app.id;
|
|
1756
2562
|
if (appName) {
|
|
1757
|
-
|
|
2563
|
+
const resolved = namespace ? this.resolveNavObjectNames(app, namespace) : app;
|
|
2564
|
+
this._registry.registerApp(resolved, id);
|
|
1758
2565
|
this.logger.debug("Registered App", { app: appName, from: id });
|
|
1759
2566
|
}
|
|
1760
2567
|
}
|
|
1761
2568
|
}
|
|
1762
2569
|
if (manifest.name && manifest.navigation && !manifest.apps?.length) {
|
|
1763
|
-
|
|
2570
|
+
const resolved = namespace ? this.resolveNavObjectNames(manifest, namespace) : manifest;
|
|
2571
|
+
this._registry.registerApp(resolved, id);
|
|
1764
2572
|
this.logger.debug("Registered manifest-as-app", { app: manifest.name, from: id });
|
|
1765
2573
|
}
|
|
1766
2574
|
const metadataArrayKeys = [
|
|
@@ -1799,9 +2607,12 @@ var _ObjectQL = class _ObjectQL {
|
|
|
1799
2607
|
if (Array.isArray(items) && items.length > 0) {
|
|
1800
2608
|
this.logger.debug(`Registering ${key} from manifest`, { id, count: items.length });
|
|
1801
2609
|
for (const item of items) {
|
|
1802
|
-
const itemName =
|
|
2610
|
+
const itemName = resolveMetadataItemName(key, item);
|
|
1803
2611
|
if (itemName) {
|
|
1804
|
-
|
|
2612
|
+
const toRegister = item.name === itemName ? item : { ...item, name: itemName };
|
|
2613
|
+
this._registry.registerItem(pluralToSingular(key), toRegister, "name", id);
|
|
2614
|
+
} else {
|
|
2615
|
+
this.logger.warn(`Skipping ${pluralToSingular(key)} without a derivable name`, { id });
|
|
1805
2616
|
}
|
|
1806
2617
|
}
|
|
1807
2618
|
}
|
|
@@ -1811,14 +2622,14 @@ var _ObjectQL = class _ObjectQL {
|
|
|
1811
2622
|
this.logger.debug("Registering seed data datasets", { id, count: seedData.length });
|
|
1812
2623
|
for (const dataset of seedData) {
|
|
1813
2624
|
if (dataset.object) {
|
|
1814
|
-
|
|
2625
|
+
this._registry.registerItem("data", dataset, "object", id);
|
|
1815
2626
|
}
|
|
1816
2627
|
}
|
|
1817
2628
|
}
|
|
1818
2629
|
if (manifest.contributes?.kinds) {
|
|
1819
2630
|
this.logger.debug("Registering kinds from manifest", { id, kindCount: manifest.contributes.kinds.length });
|
|
1820
2631
|
for (const kind of manifest.contributes.kinds) {
|
|
1821
|
-
|
|
2632
|
+
this._registry.registerKind(kind);
|
|
1822
2633
|
this.logger.debug("Registered Kind", { kind: kind.name || kind.type, from: id });
|
|
1823
2634
|
}
|
|
1824
2635
|
}
|
|
@@ -1833,6 +2644,25 @@ var _ObjectQL = class _ObjectQL {
|
|
|
1833
2644
|
}
|
|
1834
2645
|
}
|
|
1835
2646
|
}
|
|
2647
|
+
/**
|
|
2648
|
+
* Deep-clone an app definition, resolving objectName references in navigation
|
|
2649
|
+
* items via the registry. Object names are canonical identifiers — no FQN
|
|
2650
|
+
* expansion is applied.
|
|
2651
|
+
*/
|
|
2652
|
+
resolveNavObjectNames(app, namespace) {
|
|
2653
|
+
if (!app.navigation) return app;
|
|
2654
|
+
const resolveItems = (items) => items.map((item) => {
|
|
2655
|
+
const resolved = { ...item };
|
|
2656
|
+
if (resolved.objectName && !resolved.objectName.includes("__")) {
|
|
2657
|
+
resolved.objectName = computeFQN(namespace, resolved.objectName);
|
|
2658
|
+
}
|
|
2659
|
+
if (Array.isArray(resolved.children)) {
|
|
2660
|
+
resolved.children = resolveItems(resolved.children);
|
|
2661
|
+
}
|
|
2662
|
+
return resolved;
|
|
2663
|
+
});
|
|
2664
|
+
return { ...app, navigation: resolveItems(app.navigation) };
|
|
2665
|
+
}
|
|
1836
2666
|
/**
|
|
1837
2667
|
* Register a nested plugin's metadata (objects, actions, views, etc.)
|
|
1838
2668
|
*
|
|
@@ -1853,7 +2683,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
1853
2683
|
if (Array.isArray(plugin.objects)) {
|
|
1854
2684
|
this.logger.debug("Registering plugin objects (Array)", { pluginName, count: plugin.objects.length });
|
|
1855
2685
|
for (const objDef of plugin.objects) {
|
|
1856
|
-
const fqn =
|
|
2686
|
+
const fqn = this._registry.registerObject(objDef, ownerId, pluginNamespace, "own");
|
|
1857
2687
|
this.logger.debug("Registered Object", { fqn, from: pluginName });
|
|
1858
2688
|
}
|
|
1859
2689
|
} else {
|
|
@@ -1861,7 +2691,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
1861
2691
|
this.logger.debug("Registering plugin objects (Map)", { pluginName, count: entries.length });
|
|
1862
2692
|
for (const [name, objDef] of entries) {
|
|
1863
2693
|
objDef.name = name;
|
|
1864
|
-
const fqn =
|
|
2694
|
+
const fqn = this._registry.registerObject(objDef, ownerId, pluginNamespace, "own");
|
|
1865
2695
|
this.logger.debug("Registered Object", { fqn, from: pluginName });
|
|
1866
2696
|
}
|
|
1867
2697
|
}
|
|
@@ -1871,7 +2701,8 @@ var _ObjectQL = class _ObjectQL {
|
|
|
1871
2701
|
}
|
|
1872
2702
|
if (plugin.name && plugin.navigation) {
|
|
1873
2703
|
try {
|
|
1874
|
-
|
|
2704
|
+
const resolved = pluginNamespace ? this.resolveNavObjectNames(plugin, pluginNamespace) : plugin;
|
|
2705
|
+
this._registry.registerApp(resolved, ownerId);
|
|
1875
2706
|
this.logger.debug("Registered plugin-as-app", { app: plugin.name, from: pluginName });
|
|
1876
2707
|
} catch (err) {
|
|
1877
2708
|
this.logger.warn("Failed to register plugin as app", { pluginName, error: err.message });
|
|
@@ -1905,9 +2736,10 @@ var _ObjectQL = class _ObjectQL {
|
|
|
1905
2736
|
const items = plugin[key];
|
|
1906
2737
|
if (Array.isArray(items) && items.length > 0) {
|
|
1907
2738
|
for (const item of items) {
|
|
1908
|
-
const itemName =
|
|
2739
|
+
const itemName = resolveMetadataItemName(key, item);
|
|
1909
2740
|
if (itemName) {
|
|
1910
|
-
|
|
2741
|
+
const toRegister = item.name === itemName ? item : { ...item, name: itemName };
|
|
2742
|
+
this._registry.registerItem(pluralToSingular(key), toRegister, "name", ownerId);
|
|
1911
2743
|
}
|
|
1912
2744
|
}
|
|
1913
2745
|
}
|
|
@@ -1945,48 +2777,117 @@ var _ObjectQL = class _ObjectQL {
|
|
|
1945
2777
|
* Helper to get object definition
|
|
1946
2778
|
*/
|
|
1947
2779
|
getSchema(objectName) {
|
|
1948
|
-
return
|
|
2780
|
+
return this._registry.getObject(objectName);
|
|
1949
2781
|
}
|
|
1950
2782
|
/**
|
|
1951
|
-
* Resolve
|
|
1952
|
-
*
|
|
1953
|
-
*
|
|
1954
|
-
*
|
|
1955
|
-
*
|
|
1956
|
-
*
|
|
1957
|
-
* This ensures that all driver operations use a consistent key
|
|
1958
|
-
* regardless of whether the caller uses the short name or FQN.
|
|
2783
|
+
* Resolve any object identifier to the physical storage name used by drivers.
|
|
2784
|
+
*
|
|
2785
|
+
* Accepts the canonical short name (e.g., 'account') or, for explicit
|
|
2786
|
+
* cross-package disambiguation, the canonical object name (e.g., 'account'). The result is
|
|
2787
|
+
* the physical table name derived via `StorageNameMapping.resolveTableName`.
|
|
1959
2788
|
*/
|
|
1960
2789
|
resolveObjectName(name) {
|
|
1961
|
-
const schema =
|
|
2790
|
+
const schema = this._registry.getObject(name);
|
|
1962
2791
|
if (schema) {
|
|
1963
|
-
return
|
|
2792
|
+
return StorageNameMapping.resolveTableName(schema);
|
|
1964
2793
|
}
|
|
1965
|
-
return name;
|
|
2794
|
+
return StorageNameMapping.resolveTableName({ name });
|
|
1966
2795
|
}
|
|
1967
2796
|
/**
|
|
1968
2797
|
* Helper to get the target driver
|
|
2798
|
+
*
|
|
2799
|
+
* Resolution priority (first match wins):
|
|
2800
|
+
* 1. Object's explicit `datasource` field (if not 'default')
|
|
2801
|
+
* 2. DatasourceMapping rules (namespace/package/pattern matching)
|
|
2802
|
+
* 3. Package's `defaultDatasource` from manifest
|
|
2803
|
+
* 4. Global default driver
|
|
1969
2804
|
*/
|
|
1970
2805
|
getDriver(objectName) {
|
|
1971
|
-
const object =
|
|
1972
|
-
if (object) {
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
2806
|
+
const object = this._registry.getObject(objectName);
|
|
2807
|
+
if (object?.datasource && object.datasource !== "default") {
|
|
2808
|
+
if (this.drivers.has(object.datasource)) {
|
|
2809
|
+
return this.drivers.get(object.datasource);
|
|
2810
|
+
}
|
|
2811
|
+
throw new Error(`[ObjectQL] Datasource '${object.datasource}' configured for object '${objectName}' is not registered.`);
|
|
2812
|
+
}
|
|
2813
|
+
const mappedDatasource = this.resolveDatasourceFromMapping(objectName, object);
|
|
2814
|
+
if (mappedDatasource && this.drivers.has(mappedDatasource)) {
|
|
2815
|
+
this.logger.debug("Resolved datasource from mapping", {
|
|
2816
|
+
object: objectName,
|
|
2817
|
+
datasource: mappedDatasource
|
|
2818
|
+
});
|
|
2819
|
+
return this.drivers.get(mappedDatasource);
|
|
2820
|
+
}
|
|
2821
|
+
const fqn = object?.name || objectName;
|
|
2822
|
+
const owner = this._registry.getObjectOwner(fqn);
|
|
2823
|
+
if (owner?.packageId) {
|
|
2824
|
+
const manifest = this.manifests.get(owner.packageId);
|
|
2825
|
+
if (manifest?.defaultDatasource && manifest.defaultDatasource !== "default") {
|
|
2826
|
+
if (this.drivers.has(manifest.defaultDatasource)) {
|
|
2827
|
+
this.logger.debug("Resolved datasource from package manifest", {
|
|
2828
|
+
object: objectName,
|
|
2829
|
+
package: owner.packageId,
|
|
2830
|
+
datasource: manifest.defaultDatasource
|
|
2831
|
+
});
|
|
2832
|
+
return this.drivers.get(manifest.defaultDatasource);
|
|
1981
2833
|
}
|
|
1982
|
-
throw new Error(`[ObjectQL] Datasource '${datasourceName}' configured for object '${objectName}' is not registered.`);
|
|
1983
2834
|
}
|
|
1984
2835
|
}
|
|
1985
|
-
if (this.defaultDriver) {
|
|
2836
|
+
if (this.defaultDriver && this.drivers.has(this.defaultDriver)) {
|
|
1986
2837
|
return this.drivers.get(this.defaultDriver);
|
|
1987
2838
|
}
|
|
1988
2839
|
throw new Error(`[ObjectQL] No driver available for object '${objectName}'`);
|
|
1989
2840
|
}
|
|
2841
|
+
/**
|
|
2842
|
+
* Resolve datasource from mapping rules
|
|
2843
|
+
*
|
|
2844
|
+
* Rules are evaluated in order (or by priority if specified).
|
|
2845
|
+
* First matching rule wins.
|
|
2846
|
+
*/
|
|
2847
|
+
resolveDatasourceFromMapping(objectName, object) {
|
|
2848
|
+
if (!this.datasourceMapping || this.datasourceMapping.length === 0) {
|
|
2849
|
+
return null;
|
|
2850
|
+
}
|
|
2851
|
+
const sortedRules = [...this.datasourceMapping].sort((a, b) => {
|
|
2852
|
+
const aPriority = a.priority ?? 1e3;
|
|
2853
|
+
const bPriority = b.priority ?? 1e3;
|
|
2854
|
+
return aPriority - bPriority;
|
|
2855
|
+
});
|
|
2856
|
+
for (const rule of sortedRules) {
|
|
2857
|
+
if (rule.namespace && object?.namespace === rule.namespace) {
|
|
2858
|
+
return rule.datasource;
|
|
2859
|
+
}
|
|
2860
|
+
if (rule.package && object?.packageId === rule.package) {
|
|
2861
|
+
return rule.datasource;
|
|
2862
|
+
}
|
|
2863
|
+
if (rule.objectPattern && this.matchPattern(objectName, rule.objectPattern)) {
|
|
2864
|
+
return rule.datasource;
|
|
2865
|
+
}
|
|
2866
|
+
if (rule.default) {
|
|
2867
|
+
return rule.datasource;
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
return null;
|
|
2871
|
+
}
|
|
2872
|
+
/**
|
|
2873
|
+
* Simple glob pattern matching
|
|
2874
|
+
* Supports * (any chars) and ? (single char)
|
|
2875
|
+
*/
|
|
2876
|
+
matchPattern(objectName, pattern) {
|
|
2877
|
+
const regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
2878
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
2879
|
+
return regex.test(objectName);
|
|
2880
|
+
}
|
|
2881
|
+
/**
|
|
2882
|
+
* Set datasource mapping rules
|
|
2883
|
+
* Called by ObjectQLPlugin during bootstrap
|
|
2884
|
+
*/
|
|
2885
|
+
setDatasourceMapping(rules) {
|
|
2886
|
+
this.datasourceMapping = rules;
|
|
2887
|
+
this.logger.info("Datasource mapping rules configured", {
|
|
2888
|
+
ruleCount: rules.length
|
|
2889
|
+
});
|
|
2890
|
+
}
|
|
1990
2891
|
/**
|
|
1991
2892
|
* Initialize the engine and all registered drivers
|
|
1992
2893
|
*/
|
|
@@ -2039,7 +2940,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
2039
2940
|
async expandRelatedRecords(objectName, records, expand, depth = 0) {
|
|
2040
2941
|
if (!records || records.length === 0) return records;
|
|
2041
2942
|
if (depth >= _ObjectQL.MAX_EXPAND_DEPTH) return records;
|
|
2042
|
-
const objectSchema =
|
|
2943
|
+
const objectSchema = this._registry.getObject(objectName);
|
|
2043
2944
|
if (!objectSchema || !objectSchema.fields) return records;
|
|
2044
2945
|
for (const [fieldName, nestedAST] of Object.entries(expand)) {
|
|
2045
2946
|
const fieldDef = objectSchema.fields[fieldName];
|
|
@@ -2120,6 +3021,20 @@ var _ObjectQL = class _ObjectQL {
|
|
|
2120
3021
|
ast.limit = ast.top;
|
|
2121
3022
|
}
|
|
2122
3023
|
delete ast.top;
|
|
3024
|
+
const _findSchema = this._registry.getObject(object);
|
|
3025
|
+
const _findFormula = planFormulaProjection(_findSchema, ast.fields);
|
|
3026
|
+
if (_findFormula.projected) ast.fields = _findFormula.projected;
|
|
3027
|
+
if (_findSchema?.fields && Array.isArray(ast.fields) && ast.fields.length > 0) {
|
|
3028
|
+
const known = new Set(Object.keys(_findSchema.fields));
|
|
3029
|
+
known.add("id");
|
|
3030
|
+
known.add("created_at");
|
|
3031
|
+
known.add("updated_at");
|
|
3032
|
+
const filtered = ast.fields.filter((f) => {
|
|
3033
|
+
const head = String(f).split(".")[0];
|
|
3034
|
+
return known.has(head);
|
|
3035
|
+
});
|
|
3036
|
+
ast.fields = filtered.length > 0 ? filtered : void 0;
|
|
3037
|
+
}
|
|
2123
3038
|
const opCtx = {
|
|
2124
3039
|
object,
|
|
2125
3040
|
operation: "find",
|
|
@@ -2133,12 +3048,14 @@ var _ObjectQL = class _ObjectQL {
|
|
|
2133
3048
|
event: "beforeFind",
|
|
2134
3049
|
input: { ast: opCtx.ast, options: opCtx.options },
|
|
2135
3050
|
session: this.buildSession(opCtx.context),
|
|
3051
|
+
api: this.buildHookApi(opCtx.context),
|
|
2136
3052
|
transaction: opCtx.context?.transaction,
|
|
2137
3053
|
ql: this
|
|
2138
3054
|
};
|
|
2139
3055
|
await this.triggerHooks("beforeFind", hookContext);
|
|
2140
3056
|
try {
|
|
2141
3057
|
let result = await driver.find(object, hookContext.input.ast, hookContext.input.options);
|
|
3058
|
+
if (Array.isArray(result)) applyFormulaPlan(_findFormula.plan, result);
|
|
2142
3059
|
if (ast.expand && Object.keys(ast.expand).length > 0 && Array.isArray(result)) {
|
|
2143
3060
|
result = await this.expandRelatedRecords(object, result, ast.expand, 0);
|
|
2144
3061
|
}
|
|
@@ -2160,6 +3077,17 @@ var _ObjectQL = class _ObjectQL {
|
|
|
2160
3077
|
const ast = { object: objectName, ...query, limit: 1 };
|
|
2161
3078
|
delete ast.context;
|
|
2162
3079
|
delete ast.top;
|
|
3080
|
+
const _findOneSchema = this._registry.getObject(objectName);
|
|
3081
|
+
const _findOneFormula = planFormulaProjection(_findOneSchema, ast.fields);
|
|
3082
|
+
if (_findOneFormula.projected) ast.fields = _findOneFormula.projected;
|
|
3083
|
+
if (_findOneSchema?.fields && Array.isArray(ast.fields) && ast.fields.length > 0) {
|
|
3084
|
+
const known = new Set(Object.keys(_findOneSchema.fields));
|
|
3085
|
+
known.add("id");
|
|
3086
|
+
known.add("created_at");
|
|
3087
|
+
known.add("updated_at");
|
|
3088
|
+
const filtered = ast.fields.filter((f) => known.has(String(f).split(".")[0]));
|
|
3089
|
+
ast.fields = filtered.length > 0 ? filtered : void 0;
|
|
3090
|
+
}
|
|
2163
3091
|
const opCtx = {
|
|
2164
3092
|
object: objectName,
|
|
2165
3093
|
operation: "findOne",
|
|
@@ -2169,6 +3097,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
2169
3097
|
};
|
|
2170
3098
|
await this.executeWithMiddleware(opCtx, async () => {
|
|
2171
3099
|
let result = await driver.findOne(objectName, opCtx.ast);
|
|
3100
|
+
if (result != null) applyFormulaPlan(_findOneFormula.plan, [result]);
|
|
2172
3101
|
if (ast.expand && Object.keys(ast.expand).length > 0 && result != null) {
|
|
2173
3102
|
const expanded = await this.expandRelatedRecords(objectName, [result], ast.expand, 0);
|
|
2174
3103
|
result = expanded[0];
|
|
@@ -2194,20 +3123,31 @@ var _ObjectQL = class _ObjectQL {
|
|
|
2194
3123
|
event: "beforeInsert",
|
|
2195
3124
|
input: { data: opCtx.data, options: opCtx.options },
|
|
2196
3125
|
session: this.buildSession(opCtx.context),
|
|
3126
|
+
api: this.buildHookApi(opCtx.context),
|
|
2197
3127
|
transaction: opCtx.context?.transaction,
|
|
2198
3128
|
ql: this
|
|
2199
3129
|
};
|
|
2200
3130
|
await this.triggerHooks("beforeInsert", hookContext);
|
|
2201
3131
|
try {
|
|
2202
3132
|
let result;
|
|
3133
|
+
const nowSnap = /* @__PURE__ */ new Date();
|
|
2203
3134
|
if (Array.isArray(hookContext.input.data)) {
|
|
3135
|
+
const rows = hookContext.input.data.map(
|
|
3136
|
+
(row) => this.applyFieldDefaults(object, row, opCtx.context, nowSnap)
|
|
3137
|
+
);
|
|
2204
3138
|
if (driver.bulkCreate) {
|
|
2205
|
-
result = await driver.bulkCreate(object,
|
|
3139
|
+
result = await driver.bulkCreate(object, rows, hookContext.input.options);
|
|
2206
3140
|
} else {
|
|
2207
|
-
result = await Promise.all(
|
|
3141
|
+
result = await Promise.all(rows.map((item) => driver.create(object, item, hookContext.input.options)));
|
|
2208
3142
|
}
|
|
2209
3143
|
} else {
|
|
2210
|
-
|
|
3144
|
+
const row = this.applyFieldDefaults(
|
|
3145
|
+
object,
|
|
3146
|
+
hookContext.input.data,
|
|
3147
|
+
opCtx.context,
|
|
3148
|
+
nowSnap
|
|
3149
|
+
);
|
|
3150
|
+
result = await driver.create(object, row, hookContext.input.options);
|
|
2211
3151
|
}
|
|
2212
3152
|
hookContext.event = "afterInsert";
|
|
2213
3153
|
hookContext.result = result;
|
|
@@ -2274,6 +3214,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
2274
3214
|
event: "beforeUpdate",
|
|
2275
3215
|
input: { id, data: opCtx.data, options: opCtx.options },
|
|
2276
3216
|
session: this.buildSession(opCtx.context),
|
|
3217
|
+
api: this.buildHookApi(opCtx.context),
|
|
2277
3218
|
transaction: opCtx.context?.transaction,
|
|
2278
3219
|
ql: this
|
|
2279
3220
|
};
|
|
@@ -2339,6 +3280,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
2339
3280
|
event: "beforeDelete",
|
|
2340
3281
|
input: { id, options: opCtx.options },
|
|
2341
3282
|
session: this.buildSession(opCtx.context),
|
|
3283
|
+
api: this.buildHookApi(opCtx.context),
|
|
2342
3284
|
transaction: opCtx.context?.transaction,
|
|
2343
3285
|
ql: this
|
|
2344
3286
|
};
|
|
@@ -2418,18 +3360,42 @@ var _ObjectQL = class _ObjectQL {
|
|
|
2418
3360
|
groupBy: query.groupBy,
|
|
2419
3361
|
aggregations: query.aggregations
|
|
2420
3362
|
};
|
|
3363
|
+
const drv = driver;
|
|
3364
|
+
if (typeof drv.aggregate === "function") {
|
|
3365
|
+
return drv.aggregate(object, ast);
|
|
3366
|
+
}
|
|
2421
3367
|
return driver.find(object, ast);
|
|
2422
3368
|
});
|
|
2423
3369
|
return opCtx.result;
|
|
2424
3370
|
}
|
|
2425
3371
|
async execute(command, options) {
|
|
3372
|
+
let driver;
|
|
2426
3373
|
if (options?.object) {
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
3374
|
+
driver = this.getDriver(options.object);
|
|
3375
|
+
} else if (options?.datasource && this.drivers.has(options.datasource)) {
|
|
3376
|
+
driver = this.drivers.get(options.datasource);
|
|
3377
|
+
} else if (this.defaultDriver && this.drivers.has(this.defaultDriver)) {
|
|
3378
|
+
driver = this.drivers.get(this.defaultDriver);
|
|
3379
|
+
} else if (this.drivers.size === 1) {
|
|
3380
|
+
driver = this.drivers.values().next().value;
|
|
3381
|
+
}
|
|
3382
|
+
if (!driver) {
|
|
3383
|
+
throw new Error(
|
|
3384
|
+
"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."
|
|
3385
|
+
);
|
|
3386
|
+
}
|
|
3387
|
+
if (!driver.execute) {
|
|
3388
|
+
throw new Error("Selected driver does not implement execute()");
|
|
3389
|
+
}
|
|
3390
|
+
let rawCommand = command;
|
|
3391
|
+
let params = options?.args ?? options?.params;
|
|
3392
|
+
if (command && typeof command === "object" && !Array.isArray(command) && "sql" in command) {
|
|
3393
|
+
rawCommand = command.sql;
|
|
3394
|
+
if (params === void 0) {
|
|
3395
|
+
params = command.args ?? command.params;
|
|
2430
3396
|
}
|
|
2431
3397
|
}
|
|
2432
|
-
|
|
3398
|
+
return driver.execute(rawCommand, params, options);
|
|
2433
3399
|
}
|
|
2434
3400
|
// ============================================
|
|
2435
3401
|
// Compatibility / Convenience API
|
|
@@ -2450,16 +3416,16 @@ var _ObjectQL = class _ObjectQL {
|
|
|
2450
3416
|
}
|
|
2451
3417
|
}
|
|
2452
3418
|
}
|
|
2453
|
-
return
|
|
3419
|
+
return this._registry.registerObject(schema, packageId, namespace);
|
|
2454
3420
|
}
|
|
2455
3421
|
/**
|
|
2456
3422
|
* Unregister a single object by name.
|
|
2457
3423
|
*/
|
|
2458
3424
|
unregisterObject(name, packageId) {
|
|
2459
3425
|
if (packageId) {
|
|
2460
|
-
|
|
3426
|
+
this._registry.unregisterObjectsByPackage(packageId);
|
|
2461
3427
|
} else {
|
|
2462
|
-
|
|
3428
|
+
this._registry.unregisterItem("object", name);
|
|
2463
3429
|
}
|
|
2464
3430
|
}
|
|
2465
3431
|
/**
|
|
@@ -2475,7 +3441,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
2475
3441
|
*/
|
|
2476
3442
|
getConfigs() {
|
|
2477
3443
|
const result = {};
|
|
2478
|
-
const objects =
|
|
3444
|
+
const objects = this._registry.getAllObjects();
|
|
2479
3445
|
for (const obj of objects) {
|
|
2480
3446
|
if (obj.name) {
|
|
2481
3447
|
result[obj.name] = obj;
|
|
@@ -2509,10 +3475,32 @@ var _ObjectQL = class _ObjectQL {
|
|
|
2509
3475
|
return void 0;
|
|
2510
3476
|
}
|
|
2511
3477
|
}
|
|
3478
|
+
/**
|
|
3479
|
+
* Sync all registered object schemas to their respective drivers.
|
|
3480
|
+
* Call this after dynamically registering new objects at runtime
|
|
3481
|
+
* (e.g. after template seeding) to ensure tables/collections exist
|
|
3482
|
+
* before inserting seed data.
|
|
3483
|
+
*/
|
|
3484
|
+
async syncSchemas() {
|
|
3485
|
+
const allObjects = this._registry.getAllObjects();
|
|
3486
|
+
for (const obj of allObjects) {
|
|
3487
|
+
const driver = this.getDriverForObject(obj.name);
|
|
3488
|
+
if (!driver) continue;
|
|
3489
|
+
const tableName = StorageNameMapping.resolveTableName(obj);
|
|
3490
|
+
if (typeof driver.syncSchemasBatch === "function" && driver.supports?.batchSchemaSync) {
|
|
3491
|
+
}
|
|
3492
|
+
if (typeof driver.syncSchema === "function") {
|
|
3493
|
+
try {
|
|
3494
|
+
await driver.syncSchema(tableName, obj);
|
|
3495
|
+
} catch {
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3499
|
+
}
|
|
2512
3500
|
/**
|
|
2513
3501
|
* Get a registered driver by datasource name.
|
|
2514
3502
|
* Alias matching @objectql/core datasource() API.
|
|
2515
|
-
*
|
|
3503
|
+
*
|
|
2516
3504
|
* @throws Error if the datasource is not found
|
|
2517
3505
|
*/
|
|
2518
3506
|
datasource(name) {
|
|
@@ -2543,7 +3531,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
2543
3531
|
}
|
|
2544
3532
|
}
|
|
2545
3533
|
this.removeActionsByPackage(packageId);
|
|
2546
|
-
|
|
3534
|
+
this._registry.unregisterObjectsByPackage(packageId, true);
|
|
2547
3535
|
}
|
|
2548
3536
|
/**
|
|
2549
3537
|
* Gracefully shut down the engine, disconnecting all drivers.
|
|
@@ -2759,83 +3747,87 @@ var ScopedContext = class _ScopedContext {
|
|
|
2759
3747
|
|
|
2760
3748
|
// src/metadata-facade.ts
|
|
2761
3749
|
var MetadataFacade = class {
|
|
3750
|
+
constructor(registry) {
|
|
3751
|
+
this.registry = registry;
|
|
3752
|
+
}
|
|
2762
3753
|
/**
|
|
2763
3754
|
* Register a metadata item
|
|
2764
3755
|
*/
|
|
2765
3756
|
async register(type, name, data) {
|
|
2766
3757
|
const definition = typeof data === "object" && data !== null ? { ...data, name: data.name ?? name } : data;
|
|
2767
3758
|
if (type === "object") {
|
|
2768
|
-
|
|
3759
|
+
this.registry.registerItem(type, definition, "name");
|
|
2769
3760
|
} else {
|
|
2770
|
-
|
|
3761
|
+
this.registry.registerItem(type, definition, definition.id ? "id" : "name");
|
|
2771
3762
|
}
|
|
2772
3763
|
}
|
|
2773
3764
|
/**
|
|
2774
3765
|
* Get a metadata item by type and name
|
|
2775
3766
|
*/
|
|
2776
3767
|
async get(type, name) {
|
|
2777
|
-
const item =
|
|
3768
|
+
const item = this.registry.getItem(type, name);
|
|
2778
3769
|
return item?.content ?? item;
|
|
2779
3770
|
}
|
|
2780
3771
|
/**
|
|
2781
3772
|
* Get the raw entry (with metadata wrapper)
|
|
2782
3773
|
*/
|
|
2783
3774
|
getEntry(type, name) {
|
|
2784
|
-
return
|
|
3775
|
+
return this.registry.getItem(type, name);
|
|
2785
3776
|
}
|
|
2786
3777
|
/**
|
|
2787
3778
|
* List all items of a type
|
|
2788
3779
|
*/
|
|
2789
3780
|
async list(type) {
|
|
2790
|
-
const items =
|
|
3781
|
+
const items = this.registry.listItems(type);
|
|
2791
3782
|
return items.map((item) => item?.content ?? item);
|
|
2792
3783
|
}
|
|
2793
3784
|
/**
|
|
2794
3785
|
* Unregister a metadata item
|
|
2795
3786
|
*/
|
|
2796
3787
|
async unregister(type, name) {
|
|
2797
|
-
|
|
3788
|
+
this.registry.unregisterItem(type, name);
|
|
2798
3789
|
}
|
|
2799
3790
|
/**
|
|
2800
3791
|
* Check if a metadata item exists
|
|
2801
3792
|
*/
|
|
2802
3793
|
async exists(type, name) {
|
|
2803
|
-
const item =
|
|
3794
|
+
const item = this.registry.getItem(type, name);
|
|
2804
3795
|
return item !== void 0 && item !== null;
|
|
2805
3796
|
}
|
|
2806
3797
|
/**
|
|
2807
3798
|
* List all names of metadata items of a given type
|
|
2808
3799
|
*/
|
|
2809
3800
|
async listNames(type) {
|
|
2810
|
-
const items =
|
|
3801
|
+
const items = this.registry.listItems(type);
|
|
2811
3802
|
return items.map((item) => item?.name ?? item?.content?.name ?? "").filter(Boolean);
|
|
2812
3803
|
}
|
|
2813
3804
|
/**
|
|
2814
3805
|
* Unregister all metadata from a package
|
|
2815
3806
|
*/
|
|
2816
3807
|
async unregisterPackage(packageName) {
|
|
2817
|
-
|
|
3808
|
+
this.registry.unregisterObjectsByPackage(packageName);
|
|
2818
3809
|
}
|
|
2819
3810
|
/**
|
|
2820
3811
|
* Convenience: get object definition
|
|
2821
3812
|
*/
|
|
2822
3813
|
async getObject(name) {
|
|
2823
|
-
return
|
|
3814
|
+
return this.registry.getObject(name);
|
|
2824
3815
|
}
|
|
2825
3816
|
/**
|
|
2826
3817
|
* Convenience: list all objects
|
|
2827
3818
|
*/
|
|
2828
3819
|
async listObjects() {
|
|
2829
|
-
return
|
|
3820
|
+
return this.registry.getAllObjects();
|
|
2830
3821
|
}
|
|
2831
3822
|
};
|
|
2832
3823
|
|
|
2833
3824
|
// src/plugin.ts
|
|
3825
|
+
import { StorageNameMapping as StorageNameMapping2 } from "@objectstack/spec/system";
|
|
2834
3826
|
function hasLoadMetaFromDb(service) {
|
|
2835
3827
|
return typeof service === "object" && service !== null && typeof service["loadMetaFromDb"] === "function";
|
|
2836
3828
|
}
|
|
2837
3829
|
var ObjectQLPlugin = class {
|
|
2838
|
-
constructor(
|
|
3830
|
+
constructor(qlOrOptions, hostContext) {
|
|
2839
3831
|
this.name = "com.objectstack.engine.objectql";
|
|
2840
3832
|
this.type = "objectql";
|
|
2841
3833
|
this.version = "1.0.0";
|
|
@@ -2860,7 +3852,9 @@ var ObjectQLPlugin = class {
|
|
|
2860
3852
|
});
|
|
2861
3853
|
const protocolShim = new ObjectStackProtocolImplementation(
|
|
2862
3854
|
this.ql,
|
|
2863
|
-
() => ctx.getServices ? ctx.getServices() : /* @__PURE__ */ new Map()
|
|
3855
|
+
() => ctx.getServices ? ctx.getServices() : /* @__PURE__ */ new Map(),
|
|
3856
|
+
void 0,
|
|
3857
|
+
this.projectId
|
|
2864
3858
|
);
|
|
2865
3859
|
ctx.registerService("protocol", protocolShim);
|
|
2866
3860
|
ctx.logger.info("Protocol service registered");
|
|
@@ -2903,103 +3897,172 @@ var ObjectQLPlugin = class {
|
|
|
2903
3897
|
}
|
|
2904
3898
|
}
|
|
2905
3899
|
await this.ql?.init();
|
|
2906
|
-
await this.restoreMetadataFromDb(ctx);
|
|
2907
3900
|
await this.syncRegisteredSchemas(ctx);
|
|
2908
|
-
|
|
3901
|
+
if (this.projectId === void 0) {
|
|
3902
|
+
await this.restoreMetadataFromDb(ctx);
|
|
3903
|
+
} else {
|
|
3904
|
+
ctx.logger.info("Project kernel \u2014 skipping sys_metadata hydration (metadata sourced from artifact)");
|
|
3905
|
+
}
|
|
3906
|
+
await this.syncRegisteredSchemas(ctx);
|
|
3907
|
+
if (this.projectId === void 0) {
|
|
3908
|
+
await this.bridgeObjectsToMetadataService(ctx);
|
|
3909
|
+
}
|
|
2909
3910
|
this.registerAuditHooks(ctx);
|
|
2910
|
-
this.registerTenantMiddleware(ctx);
|
|
2911
3911
|
ctx.logger.info("ObjectQL engine started", {
|
|
2912
3912
|
driversRegistered: this.ql?.["drivers"]?.size || 0,
|
|
2913
3913
|
objectsRegistered: this.ql?.registry?.getAllObjects?.()?.length || 0
|
|
2914
3914
|
});
|
|
2915
3915
|
};
|
|
2916
|
-
if (
|
|
2917
|
-
this.ql =
|
|
2918
|
-
} else {
|
|
3916
|
+
if (qlOrOptions instanceof ObjectQL) {
|
|
3917
|
+
this.ql = qlOrOptions;
|
|
2919
3918
|
this.hostContext = hostContext;
|
|
3919
|
+
return;
|
|
2920
3920
|
}
|
|
3921
|
+
const opts = qlOrOptions ?? {};
|
|
3922
|
+
if (opts.ql) {
|
|
3923
|
+
this.ql = opts.ql;
|
|
3924
|
+
}
|
|
3925
|
+
this.hostContext = opts.hostContext ?? hostContext;
|
|
3926
|
+
this.projectId = opts.projectId;
|
|
2921
3927
|
}
|
|
2922
3928
|
/**
|
|
2923
3929
|
* Register built-in audit hooks for auto-stamping created_by/updated_by
|
|
2924
|
-
* and fetching previousData for update/delete operations.
|
|
3930
|
+
* and fetching previousData for update/delete operations. These are
|
|
3931
|
+
* declared as canonical `Hook` metadata and bound through the same
|
|
3932
|
+
* `bindHooksToEngine` path used by `defineStack({ hooks })`, so the
|
|
3933
|
+
* engine's built-ins flow through the same rails as user code
|
|
3934
|
+
* (dogfooding the protocol).
|
|
2925
3935
|
*/
|
|
2926
3936
|
registerAuditHooks(ctx) {
|
|
2927
3937
|
if (!this.ql) return;
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
3938
|
+
const stamp = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
3939
|
+
const hasField = (objectName, field) => {
|
|
3940
|
+
try {
|
|
3941
|
+
const schema = this.ql?.getSchema?.(objectName);
|
|
3942
|
+
if (!schema || typeof schema !== "object") return false;
|
|
3943
|
+
const fields = schema.fields;
|
|
3944
|
+
if (!fields || typeof fields !== "object") return false;
|
|
3945
|
+
return Object.prototype.hasOwnProperty.call(fields, field);
|
|
3946
|
+
} catch {
|
|
3947
|
+
return false;
|
|
3948
|
+
}
|
|
3949
|
+
};
|
|
3950
|
+
const applyToRecord = (record, objectName, session, isInsert) => {
|
|
3951
|
+
const now = stamp();
|
|
3952
|
+
if (isInsert) {
|
|
3953
|
+
record.created_at = record.created_at ?? now;
|
|
3954
|
+
}
|
|
3955
|
+
record.updated_at = now;
|
|
3956
|
+
if (session?.userId) {
|
|
3957
|
+
if (isInsert && hasField(objectName, "created_by")) {
|
|
3958
|
+
record.created_by = record.created_by ?? session.userId;
|
|
3959
|
+
}
|
|
3960
|
+
if (hasField(objectName, "updated_by")) {
|
|
3961
|
+
record.updated_by = session.userId;
|
|
2939
3962
|
}
|
|
2940
3963
|
}
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
3964
|
+
if (isInsert && session?.tenantId && hasField(objectName, "tenant_id")) {
|
|
3965
|
+
record.tenant_id = record.tenant_id ?? session.tenantId;
|
|
3966
|
+
}
|
|
3967
|
+
};
|
|
3968
|
+
const stampData = (data, objectName, session, isInsert) => {
|
|
3969
|
+
if (Array.isArray(data)) {
|
|
3970
|
+
for (const row of data) {
|
|
3971
|
+
if (row && typeof row === "object") {
|
|
3972
|
+
applyToRecord(row, objectName, session, isInsert);
|
|
3973
|
+
}
|
|
2948
3974
|
}
|
|
3975
|
+
} else if (data && typeof data === "object") {
|
|
3976
|
+
applyToRecord(data, objectName, session, isInsert);
|
|
2949
3977
|
}
|
|
2950
|
-
}
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
3978
|
+
};
|
|
3979
|
+
const builtinHooks = [
|
|
3980
|
+
{
|
|
3981
|
+
name: "sys_stamp_audit_insert",
|
|
3982
|
+
object: "*",
|
|
3983
|
+
events: ["beforeInsert"],
|
|
3984
|
+
priority: 10,
|
|
3985
|
+
description: "Auto-stamp created_by / updated_by / created_at / updated_at / tenant_id on insert (only when the field exists on the object schema)",
|
|
3986
|
+
handler: async (hookCtx) => {
|
|
3987
|
+
if (hookCtx.input?.data) {
|
|
3988
|
+
stampData(hookCtx.input.data, hookCtx.object, hookCtx.session, true);
|
|
3989
|
+
}
|
|
3990
|
+
}
|
|
3991
|
+
},
|
|
3992
|
+
{
|
|
3993
|
+
name: "sys_stamp_audit_update",
|
|
3994
|
+
object: "*",
|
|
3995
|
+
events: ["beforeUpdate"],
|
|
3996
|
+
priority: 10,
|
|
3997
|
+
description: "Auto-stamp updated_by / updated_at on update (only when the field exists on the object schema)",
|
|
3998
|
+
handler: async (hookCtx) => {
|
|
3999
|
+
if (hookCtx.input?.data) {
|
|
4000
|
+
stampData(hookCtx.input.data, hookCtx.object, hookCtx.session, false);
|
|
4001
|
+
}
|
|
4002
|
+
}
|
|
4003
|
+
},
|
|
4004
|
+
{
|
|
4005
|
+
name: "sys_fetch_previous_update",
|
|
4006
|
+
object: "*",
|
|
4007
|
+
events: ["beforeUpdate"],
|
|
4008
|
+
priority: 5,
|
|
4009
|
+
description: "Auto-fetch the previous record for update hooks",
|
|
4010
|
+
handler: async (hookCtx) => {
|
|
4011
|
+
if (hookCtx.input?.id && !hookCtx.previous) {
|
|
4012
|
+
try {
|
|
4013
|
+
const existing = await this.ql.findOne(hookCtx.object, {
|
|
4014
|
+
where: { id: hookCtx.input.id }
|
|
4015
|
+
});
|
|
4016
|
+
if (existing) hookCtx.previous = existing;
|
|
4017
|
+
} catch (_e) {
|
|
4018
|
+
}
|
|
4019
|
+
}
|
|
4020
|
+
}
|
|
4021
|
+
},
|
|
4022
|
+
{
|
|
4023
|
+
name: "sys_fetch_previous_delete",
|
|
4024
|
+
object: "*",
|
|
4025
|
+
events: ["beforeDelete"],
|
|
4026
|
+
priority: 5,
|
|
4027
|
+
description: "Auto-fetch the previous record for delete hooks",
|
|
4028
|
+
handler: async (hookCtx) => {
|
|
4029
|
+
if (hookCtx.input?.id && !hookCtx.previous) {
|
|
4030
|
+
try {
|
|
4031
|
+
const existing = await this.ql.findOne(hookCtx.object, {
|
|
4032
|
+
where: { id: hookCtx.input.id }
|
|
4033
|
+
});
|
|
4034
|
+
if (existing) hookCtx.previous = existing;
|
|
4035
|
+
} catch (_e) {
|
|
4036
|
+
}
|
|
2959
4037
|
}
|
|
2960
|
-
} catch (_e) {
|
|
2961
4038
|
}
|
|
2962
4039
|
}
|
|
2963
|
-
|
|
2964
|
-
this.ql.
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
4040
|
+
];
|
|
4041
|
+
if (typeof this.ql.bindHooks === "function") {
|
|
4042
|
+
this.ql.bindHooks(builtinHooks, { packageId: "sys:audit" });
|
|
4043
|
+
} else {
|
|
4044
|
+
for (const h of builtinHooks) {
|
|
4045
|
+
for (const event of h.events) {
|
|
4046
|
+
this.ql.registerHook(event, h.handler, {
|
|
4047
|
+
object: h.object,
|
|
4048
|
+
priority: h.priority,
|
|
4049
|
+
packageId: "sys:audit"
|
|
2969
4050
|
});
|
|
2970
|
-
if (existing) {
|
|
2971
|
-
hookCtx.previous = existing;
|
|
2972
|
-
}
|
|
2973
|
-
} catch (_e) {
|
|
2974
4051
|
}
|
|
2975
4052
|
}
|
|
2976
|
-
}
|
|
2977
|
-
ctx.logger.debug("Audit hooks registered (created_by/updated_by, previousData)");
|
|
4053
|
+
}
|
|
4054
|
+
ctx.logger.debug("Audit hooks registered via binder (created_by/updated_by, previousData)");
|
|
2978
4055
|
}
|
|
2979
4056
|
/**
|
|
2980
|
-
*
|
|
2981
|
-
*
|
|
4057
|
+
* Tenant isolation moved to `@objectstack/plugin-security`'s
|
|
4058
|
+
* `member_default` permission set RLS
|
|
4059
|
+
* (`organization_id = current_user.organization_id`, with
|
|
4060
|
+
* field-existence guards). The legacy `registerTenantMiddleware`
|
|
4061
|
+
* method was removed because it (a) collided with SecurityPlugin's
|
|
4062
|
+
* RLS pipeline and (b) blindly filtered tables that don't have a
|
|
4063
|
+
* `tenant_id` column (e.g. `sys_organization`), returning 0 rows
|
|
4064
|
+
* instead of all rows.
|
|
2982
4065
|
*/
|
|
2983
|
-
registerTenantMiddleware(ctx) {
|
|
2984
|
-
if (!this.ql) return;
|
|
2985
|
-
this.ql.registerMiddleware(async (opCtx, next) => {
|
|
2986
|
-
if (!opCtx.context?.tenantId || opCtx.context?.isSystem) {
|
|
2987
|
-
return next();
|
|
2988
|
-
}
|
|
2989
|
-
if (["find", "findOne", "count", "aggregate"].includes(opCtx.operation)) {
|
|
2990
|
-
if (opCtx.ast) {
|
|
2991
|
-
const tenantFilter = { tenant_id: opCtx.context.tenantId };
|
|
2992
|
-
if (opCtx.ast.where) {
|
|
2993
|
-
opCtx.ast.where = { $and: [opCtx.ast.where, tenantFilter] };
|
|
2994
|
-
} else {
|
|
2995
|
-
opCtx.ast.where = tenantFilter;
|
|
2996
|
-
}
|
|
2997
|
-
}
|
|
2998
|
-
}
|
|
2999
|
-
await next();
|
|
3000
|
-
});
|
|
3001
|
-
ctx.logger.debug("Tenant isolation middleware registered");
|
|
3002
|
-
}
|
|
3003
4066
|
/**
|
|
3004
4067
|
* Synchronize all registered object schemas to the database.
|
|
3005
4068
|
*
|
|
@@ -3038,7 +4101,7 @@ var ObjectQLPlugin = class {
|
|
|
3038
4101
|
skipped++;
|
|
3039
4102
|
continue;
|
|
3040
4103
|
}
|
|
3041
|
-
const tableName =
|
|
4104
|
+
const tableName = StorageNameMapping2.resolveTableName(obj);
|
|
3042
4105
|
let group = driverGroups.get(driver);
|
|
3043
4106
|
if (!group) {
|
|
3044
4107
|
group = [];
|
|
@@ -3195,13 +4258,20 @@ var ObjectQLPlugin = class {
|
|
|
3195
4258
|
*/
|
|
3196
4259
|
async loadMetadataFromService(metadataService, ctx) {
|
|
3197
4260
|
ctx.logger.info("Syncing metadata from external service into ObjectQL registry...");
|
|
3198
|
-
const metadataTypes = ["object", "view", "app", "flow", "workflow", "function"];
|
|
4261
|
+
const metadataTypes = ["object", "view", "app", "flow", "workflow", "function", "hook"];
|
|
3199
4262
|
let totalLoaded = 0;
|
|
3200
4263
|
for (const type of metadataTypes) {
|
|
3201
4264
|
try {
|
|
3202
4265
|
if (typeof metadataService.loadMany === "function") {
|
|
3203
4266
|
const items = await metadataService.loadMany(type);
|
|
3204
4267
|
if (items && items.length > 0) {
|
|
4268
|
+
if (type === "function" && this.ql && typeof this.ql.registerFunction === "function") {
|
|
4269
|
+
for (const item of items) {
|
|
4270
|
+
if (item?.name && typeof item.handler === "function") {
|
|
4271
|
+
this.ql.registerFunction(item.name, item.handler, "metadata-service");
|
|
4272
|
+
}
|
|
4273
|
+
}
|
|
4274
|
+
}
|
|
3205
4275
|
items.forEach((item) => {
|
|
3206
4276
|
const keyField = item.id ? "id" : "name";
|
|
3207
4277
|
if (type === "object" && this.ql) {
|
|
@@ -3211,6 +4281,11 @@ var ObjectQLPlugin = class {
|
|
|
3211
4281
|
this.ql.registry.registerItem(type, item, keyField);
|
|
3212
4282
|
}
|
|
3213
4283
|
});
|
|
4284
|
+
if (type === "hook" && this.ql && typeof this.ql.bindHooks === "function") {
|
|
4285
|
+
this.ql.bindHooks(items, {
|
|
4286
|
+
packageId: "metadata-service"
|
|
4287
|
+
});
|
|
4288
|
+
}
|
|
3214
4289
|
totalLoaded += items.length;
|
|
3215
4290
|
ctx.logger.info(`Synced ${items.length} ${type}(s) from metadata service`);
|
|
3216
4291
|
}
|
|
@@ -3326,6 +4401,7 @@ function convertIntrospectedSchemaToObjects(introspectedSchema, options) {
|
|
|
3326
4401
|
export {
|
|
3327
4402
|
DEFAULT_EXTENDER_PRIORITY,
|
|
3328
4403
|
DEFAULT_OWNER_PRIORITY,
|
|
4404
|
+
InMemoryHookMetricsRecorder,
|
|
3329
4405
|
MetadataFacade,
|
|
3330
4406
|
ObjectQL,
|
|
3331
4407
|
ObjectQLPlugin,
|
|
@@ -3334,10 +4410,14 @@ export {
|
|
|
3334
4410
|
RESERVED_NAMESPACES,
|
|
3335
4411
|
SchemaRegistry,
|
|
3336
4412
|
ScopedContext,
|
|
4413
|
+
applySystemFields,
|
|
4414
|
+
bindHooksToEngine,
|
|
3337
4415
|
computeFQN,
|
|
3338
4416
|
convertIntrospectedSchemaToObjects,
|
|
3339
4417
|
createObjectQLKernel,
|
|
4418
|
+
noopHookMetricsRecorder,
|
|
3340
4419
|
parseFQN,
|
|
3341
|
-
toTitleCase
|
|
4420
|
+
toTitleCase,
|
|
4421
|
+
wrapDeclarativeHook
|
|
3342
4422
|
};
|
|
3343
4423
|
//# sourceMappingURL=index.mjs.map
|