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