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