@objectstack/objectql 9.1.0 → 9.3.0

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
@@ -886,6 +886,22 @@ function applySystemFields(schema, opts) {
886
886
  fields: { ...additions, ...schema.fields ?? {} }
887
887
  };
888
888
  }
889
+ var SYS_METADATA_OWNER = "sys_metadata";
890
+ function isRealPackage(pkg) {
891
+ return typeof pkg === "string" && pkg.length > 0 && pkg !== SYS_METADATA_OWNER;
892
+ }
893
+ var MetadataCollisionError = class extends Error {
894
+ constructor(type, name, existingPackageId, incomingPackageId) {
895
+ super(
896
+ `Cross-package metadata collision: ${type}/${name} is registered by package "${existingPackageId}" and package "${incomingPackageId}". Bare-named ${type} metadata has no package coordinate in the registry, so the second registration would silently shadow the first (last-write-wins at read time). Rename one of them (a namespace prefix such as "<namespace>_${name}" is recommended), or, if this is a deliberate migration, set OS_METADATA_COLLISION=warn to downgrade to a warning. See ADR-0048.`
897
+ );
898
+ this.name = "MetadataCollisionError";
899
+ this.type = type;
900
+ this.name_ = name;
901
+ this.existingPackageId = existingPackageId;
902
+ this.incomingPackageId = incomingPackageId;
903
+ }
904
+ };
889
905
  var SchemaRegistry = class {
890
906
  constructor(options = {}) {
891
907
  // ==========================================
@@ -929,6 +945,7 @@ var SchemaRegistry = class {
929
945
  } else {
930
946
  this.multiTenant = String(readEnvWithDeprecation("OS_MULTI_ORG_ENABLED", "OS_MULTI_TENANT") ?? "false").toLowerCase() !== "false";
931
947
  }
948
+ this.collisionPolicy = options.collisionPolicy ?? ((process.env.OS_METADATA_COLLISION ?? "").toLowerCase() === "warn" ? "warn" : "error");
932
949
  }
933
950
  get logLevel() {
934
951
  return this._logLevel;
@@ -1236,6 +1253,17 @@ var SchemaRegistry = class {
1236
1253
  if (collection.has(storageKey)) {
1237
1254
  this.log(`[Registry] Overwriting ${type}: ${storageKey}`);
1238
1255
  }
1256
+ if (isRealPackage(packageId)) {
1257
+ const conflictOwner = this.findOtherPackageOwner(collection, baseName, packageId);
1258
+ if (conflictOwner) {
1259
+ const err = new MetadataCollisionError(type, baseName, conflictOwner, packageId);
1260
+ if (this.collisionPolicy === "warn") {
1261
+ console.warn(`[Registry] ${err.message}`);
1262
+ } else {
1263
+ throw err;
1264
+ }
1265
+ }
1266
+ }
1239
1267
  if (packageId && collection.has(baseName)) {
1240
1268
  const dbOnly = collection.get(baseName);
1241
1269
  if (dbOnly && !dbOnly._packageId) {
@@ -1247,6 +1275,22 @@ var SchemaRegistry = class {
1247
1275
  collection.set(storageKey, item);
1248
1276
  this.log(`[Registry] Registered ${type}: ${storageKey}`);
1249
1277
  }
1278
+ /**
1279
+ * Find a code package OTHER than `incoming` that already owns `baseName` in
1280
+ * `collection` (ADR-0048 cross-package collision detection). Scans the live
1281
+ * collection — like {@link getItem} / {@link unregisterItem} — so it always
1282
+ * reflects current state with no parallel index to drift across
1283
+ * reset/unregister. Returns the conflicting owner's package id, or undefined
1284
+ * when the name is free or only held by the same package / a runtime overlay.
1285
+ */
1286
+ findOtherPackageOwner(collection, baseName, incoming) {
1287
+ for (const [key, item] of collection) {
1288
+ if (key !== baseName && !key.endsWith(`:${baseName}`)) continue;
1289
+ const owner = item?._packageId;
1290
+ if (isRealPackage(owner) && owner !== incoming) return owner;
1291
+ }
1292
+ return void 0;
1293
+ }
1250
1294
  /**
1251
1295
  * Validate Metadata against Spec Zod Schemas
1252
1296
  */
@@ -1306,6 +1350,70 @@ var SchemaRegistry = class {
1306
1350
  }
1307
1351
  return void 0;
1308
1352
  }
1353
+ /**
1354
+ * Artifact-only lookup (ADR-0010 §3.3). Unlike {@link getItem} — which
1355
+ * returns the plain-key entry first, so a runtime/DB-rehydrated row
1356
+ * registered under the bare name SHADOWS the packaged artifact — this
1357
+ * scans the composite (`<packageId>:<name>`) entries first and only
1358
+ * returns an item whose `_packageId` marks a genuine code package
1359
+ * (truthy and not the `'sys_metadata'` rehydration sentinel).
1360
+ *
1361
+ * This is what the protocol's lock/provenance resolution must use:
1362
+ * the artifact's `_lock` envelope always wins over an overlay, and an
1363
+ * overlay row hydrated into the plain key must never be able to mask
1364
+ * it (that masking is exactly the "registry pollution" bug where a
1365
+ * locked app's `_lock` read back as undefined after a PUT+GET).
1366
+ */
1367
+ getArtifactItem(type, name) {
1368
+ if (type === "object" || type === "objects") {
1369
+ const obj = this.getObject(name);
1370
+ return obj && obj._packageId && obj._packageId !== "sys_metadata" ? obj : void 0;
1371
+ }
1372
+ const collection = this.metadata.get(type);
1373
+ if (!collection) return void 0;
1374
+ for (const [key, item] of collection) {
1375
+ if (key !== name && key.endsWith(`:${name}`)) {
1376
+ const it = item;
1377
+ if (it && it._packageId && it._packageId !== "sys_metadata") return item;
1378
+ }
1379
+ }
1380
+ const direct = collection.get(name);
1381
+ if (direct && direct._packageId && direct._packageId !== "sys_metadata") {
1382
+ return direct;
1383
+ }
1384
+ return void 0;
1385
+ }
1386
+ /**
1387
+ * Remove a plain-key runtime shadow so the packaged artifact registered
1388
+ * under a composite key becomes the visible value again. Used by the
1389
+ * metadata reset path (`deleteMetaItem`): deleting the `sys_metadata`
1390
+ * overlay row must also heal the in-memory registry, otherwise the
1391
+ * stale overlay copy keeps shadowing the artifact until restart.
1392
+ *
1393
+ * Deliberately conservative: the plain-key entry is only deleted when a
1394
+ * packaged artifact still exists under a composite key, so the name
1395
+ * stays resolvable afterwards. A runtime-only item (no artifact
1396
+ * backing) is left untouched. Note the plain entry's own `_packageId`
1397
+ * is NOT consulted — the hydration path grafts the artifact envelope
1398
+ * onto the shadow (ADR-0010 §3.3), so a stamped `_packageId` does not
1399
+ * mean the plain entry IS the artifact registration; artifact loaders
1400
+ * always register under a composite key.
1401
+ */
1402
+ removeRuntimeShadow(type, name) {
1403
+ const collection = this.metadata.get(type);
1404
+ if (!collection || !collection.has(name)) return false;
1405
+ for (const [key, item] of collection) {
1406
+ if (key !== name && key.endsWith(`:${name}`)) {
1407
+ const it = item;
1408
+ if (it && it._packageId && it._packageId !== "sys_metadata") {
1409
+ collection.delete(name);
1410
+ this.log(`[Registry] Removed runtime shadow ${type}: ${name} (artifact ${it._packageId} restored)`);
1411
+ return true;
1412
+ }
1413
+ }
1414
+ }
1415
+ return false;
1416
+ }
1309
1417
  /**
1310
1418
  * Universal List Method
1311
1419
  */
@@ -2335,6 +2443,48 @@ function decorateMetadataItems(type, items) {
2335
2443
  if (!Array.isArray(items)) return items;
2336
2444
  return items.map((item) => decorateMetadataItem(type, item));
2337
2445
  }
2446
+ function fieldMap(objectDef) {
2447
+ const map = /* @__PURE__ */ new Map();
2448
+ const fields = objectDef?.fields;
2449
+ if (Array.isArray(fields)) {
2450
+ for (const f of fields) if (f?.name) map.set(f.name, f);
2451
+ } else if (fields && typeof fields === "object") {
2452
+ for (const [name, f] of Object.entries(fields)) map.set(name, f ?? {});
2453
+ }
2454
+ return map;
2455
+ }
2456
+ function computeViewReferenceDiagnostics(view, objectDef) {
2457
+ const fields = fieldMap(objectDef);
2458
+ const errors = [];
2459
+ const requireField = (name, path) => {
2460
+ if (typeof name !== "string" || !name) return;
2461
+ if (!fields.has(name)) {
2462
+ errors.push({
2463
+ path,
2464
+ message: `Field "${name}" does not exist on the source object`,
2465
+ code: "reference_not_found"
2466
+ });
2467
+ }
2468
+ };
2469
+ const userFilters = view?.userFilters;
2470
+ userFilters?.fields?.forEach((f, i) => requireField(f?.field, `userFilters.fields.${i}.field`));
2471
+ userFilters?.tabs?.forEach((t, i) => t?.filter?.forEach((r, j) => requireField(r?.field, `userFilters.tabs.${i}.filter.${j}.field`)));
2472
+ view?.tabs?.forEach((t, i) => t?.filter?.forEach((r, j) => requireField(r?.field, `tabs.${i}.filter.${j}.field`)));
2473
+ view?.filterableFields?.forEach((f, i) => requireField(f, `filterableFields.${i}`));
2474
+ const kanban = view?.kanban;
2475
+ if (kanban?.groupByField) {
2476
+ requireField(kanban.groupByField, "kanban.groupByField");
2477
+ const def = fields.get(kanban.groupByField);
2478
+ if (def && def.type && !["select", "multi-select", "boolean", "lookup", "master_detail"].includes(def.type)) {
2479
+ errors.push({
2480
+ path: "kanban.groupByField",
2481
+ message: `Field "${kanban.groupByField}" (type "${def.type}") cannot group a kanban \u2014 use a select-like field`,
2482
+ code: "invalid_binding"
2483
+ });
2484
+ }
2485
+ }
2486
+ return errors.length ? { valid: false, errors } : { valid: true };
2487
+ }
2338
2488
 
2339
2489
  // src/protocol.ts
2340
2490
  var TYPE_TO_FORM = METADATA_FORM_REGISTRY;
@@ -3157,8 +3307,13 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3157
3307
  }
3158
3308
  byName.set(data.name, data);
3159
3309
  }
3160
- if (this.environmentId === void 0) {
3161
- this.engine.registry.registerItem(request.type, data, "name");
3310
+ if (this.environmentId === void 0 && data && typeof data === "object") {
3311
+ const artifact = this.lookupArtifactItem(request.type, data.name);
3312
+ this.engine.registry.registerItem(
3313
+ request.type,
3314
+ mergeArtifactProtection(data, artifact),
3315
+ "name"
3316
+ );
3162
3317
  }
3163
3318
  }
3164
3319
  items = Array.from(byName.values());
@@ -3364,10 +3519,34 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3364
3519
  item = this.engine.registry.applyNavContributions(item);
3365
3520
  }
3366
3521
  const artifactItem = this.lookupArtifactItem(request.type, request.name);
3367
- const decorated = decorateMetadataItem(
3522
+ let decorated = decorateMetadataItem(
3368
3523
  request.type,
3369
3524
  mergeArtifactProtection(item, artifactItem)
3370
3525
  );
3526
+ if ((request.type === "view" || request.type === "views") && decorated && typeof decorated === "object") {
3527
+ try {
3528
+ const viewDoc = decorated;
3529
+ const sourceObject = viewDoc?.object ?? viewDoc?.data?.object ?? viewDoc?.objectName ?? viewDoc?.list?.data?.object;
3530
+ const objectDef = typeof sourceObject === "string" ? this.engine.registry.getObject(sourceObject) : void 0;
3531
+ if (objectDef) {
3532
+ const refs = computeViewReferenceDiagnostics(viewDoc, objectDef);
3533
+ if (!refs.valid) {
3534
+ const prior = viewDoc._diagnostics;
3535
+ decorated = {
3536
+ ...viewDoc,
3537
+ _diagnostics: {
3538
+ valid: false,
3539
+ errors: [
3540
+ ...prior && prior.valid === false && Array.isArray(prior.errors) ? prior.errors : [],
3541
+ ...refs.errors ?? []
3542
+ ]
3543
+ }
3544
+ };
3545
+ }
3546
+ }
3547
+ } catch {
3548
+ }
3549
+ }
3371
3550
  const artifactBacked = this.isArtifactBacked(request.type, request.name);
3372
3551
  const lockState = resolveLockState(decorated, artifactBacked);
3373
3552
  return {
@@ -3417,7 +3596,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3417
3596
  } catch {
3418
3597
  }
3419
3598
  if (code === null) {
3420
- let regItem = this.engine.registry.getItem(request.type, request.name);
3599
+ let regItem = this.lookupArtifactItem(request.type, request.name) ?? this.engine.registry.getItem(request.type, request.name);
3421
3600
  if (regItem === void 0) {
3422
3601
  const alt = PLURAL_TO_SINGULAR3[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
3423
3602
  if (alt) regItem = this.engine.registry.getItem(alt, request.name);
@@ -4463,14 +4642,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
4463
4642
  * "authoring a DB-only item" (requires only `allowRuntimeCreate`).
4464
4643
  */
4465
4644
  isArtifactBacked(type, name) {
4466
- const registry = this.engine?.registry;
4467
- if (!registry || typeof registry.getItem !== "function") {
4468
- return false;
4469
- }
4470
- const singular = PLURAL_TO_SINGULAR3[type] ?? type;
4471
- const item = registry.getItem(singular, name) ?? registry.getItem(type, name);
4472
- if (!item || !item._packageId) return false;
4473
- return item._packageId !== "sys_metadata";
4645
+ return this.lookupArtifactItem(type, name) !== void 0;
4474
4646
  }
4475
4647
  // ───────────────────────────────────────────────────────────────────
4476
4648
  // ADR-0010 — metadata protection (Phase 1: L3 item-level lock)
@@ -4482,9 +4654,17 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
4482
4654
  */
4483
4655
  lookupArtifactItem(type, name) {
4484
4656
  const registry = this.engine?.registry;
4485
- if (!registry || typeof registry.getItem !== "function") return void 0;
4657
+ if (!registry) return void 0;
4486
4658
  const singular = PLURAL_TO_SINGULAR3[type] ?? type;
4487
- return registry.getItem(singular, name) ?? registry.getItem(type, name);
4659
+ if (typeof registry.getArtifactItem === "function") {
4660
+ return registry.getArtifactItem(singular, name) ?? registry.getArtifactItem(type, name);
4661
+ }
4662
+ if (typeof registry.getItem !== "function") return void 0;
4663
+ const item = registry.getItem(singular, name) ?? registry.getItem(type, name);
4664
+ if (!item || !item._packageId || item._packageId === "sys_metadata") {
4665
+ return void 0;
4666
+ }
4667
+ return item;
4488
4668
  }
4489
4669
  /**
4490
4670
  * Resolve the effective `_lock` for an item by consulting the
@@ -4498,13 +4678,8 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
4498
4678
  * scope and the caller is expected to also gate on `environmentId`.
4499
4679
  */
4500
4680
  async getEffectiveLock(type, name, organizationId) {
4501
- const registry = this.engine?.registry;
4502
- const singular = PLURAL_TO_SINGULAR3[type] ?? type;
4503
- let artifactItem;
4504
- if (registry && typeof registry.getItem === "function") {
4505
- artifactItem = registry.getItem(singular, name) ?? registry.getItem(type, name);
4506
- }
4507
- if (artifactItem && artifactItem._packageId && artifactItem._packageId !== "sys_metadata") {
4681
+ const artifactItem = this.lookupArtifactItem(type, name);
4682
+ if (artifactItem) {
4508
4683
  const p = extractProtection(artifactItem);
4509
4684
  if (p.lock !== "none") {
4510
4685
  return { lock: p.lock, lockReason: p.lockReason, lockSource: "artifact" };
@@ -4645,6 +4820,49 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
4645
4820
  );
4646
4821
  }
4647
4822
  }
4823
+ /**
4824
+ * Heal the in-memory registry after a metadata reset (overlay-row
4825
+ * delete) on control-plane kernels. Two layers:
4826
+ *
4827
+ * 1. Drop the plain-key runtime shadow so the packaged artifact
4828
+ * (registered under `<packageId>:<name>`) becomes the visible
4829
+ * value again. The shadow is written by the overlay-hydration
4830
+ * paths (`getMetaItems` / `loadMetaFromDb`) and — pre-fix —
4831
+ * survived the reset until restart, leaving stale overlay
4832
+ * content (and a stripped `_lock` envelope) in every
4833
+ * registry-direct read (ADR-0010 §3.3).
4834
+ * 2. When no composite-key artifact exists, fall back to the
4835
+ * MetadataService baseline (FilesystemLoader-sourced types) and
4836
+ * re-register it, preserving the historical refresh behaviour
4837
+ * for items the SchemaRegistry never held as artifacts.
4838
+ *
4839
+ * Best-effort: a failure must never block the delete that already
4840
+ * succeeded; the next full reload fixes the registry anyway.
4841
+ */
4842
+ async restoreArtifactRegistryView(type, name) {
4843
+ try {
4844
+ const registry = this.engine.registry;
4845
+ let healed = false;
4846
+ if (typeof registry.removeRuntimeShadow === "function") {
4847
+ const singular = PLURAL_TO_SINGULAR3[type] ?? type;
4848
+ healed = registry.removeRuntimeShadow(singular, name);
4849
+ if (type !== singular) {
4850
+ healed = registry.removeRuntimeShadow(type, name) || healed;
4851
+ }
4852
+ }
4853
+ if (healed) return;
4854
+ if (this.environmentId !== void 0) return;
4855
+ const services = this.getServicesRegistry?.();
4856
+ const metadataService = services?.get("metadata");
4857
+ if (metadataService && typeof metadataService.get === "function") {
4858
+ const artifactItem = await metadataService.get(type, name);
4859
+ if (artifactItem !== void 0) {
4860
+ this.engine.registry.registerItem(type, artifactItem, "name");
4861
+ }
4862
+ }
4863
+ } catch {
4864
+ }
4865
+ }
4648
4866
  /**
4649
4867
  * Ensure a just-PUBLISHED object's physical table exists so it is usable
4650
4868
  * for data CRUD immediately — without a server restart. Registering the
@@ -5466,6 +5684,9 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
5466
5684
  const targetState = request.state === "draft" ? "draft" : "active";
5467
5685
  const current = await repo.get(ref, { state: targetState });
5468
5686
  if (!current) {
5687
+ if (targetState === "active") {
5688
+ await this.restoreArtifactRegistryView(request.type, request.name);
5689
+ }
5469
5690
  return {
5470
5691
  success: true,
5471
5692
  reset: false,
@@ -5480,18 +5701,8 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
5480
5701
  intent: this.isArtifactBacked(singularTypeForRepo, request.name) ? "override-artifact" : "runtime-only",
5481
5702
  state: targetState
5482
5703
  });
5483
- if (this.environmentId === void 0) {
5484
- try {
5485
- const services = this.getServicesRegistry?.();
5486
- const metadataService = services?.get("metadata");
5487
- if (metadataService && typeof metadataService.get === "function") {
5488
- const artifactItem = await metadataService.get(request.type, request.name);
5489
- if (artifactItem !== void 0) {
5490
- this.engine.registry.registerItem(request.type, artifactItem, "name");
5491
- }
5492
- }
5493
- } catch {
5494
- }
5704
+ if (targetState === "active") {
5705
+ await this.restoreArtifactRegistryView(request.type, request.name);
5495
5706
  }
5496
5707
  if (this.shouldDropStorage(request.type, request.name, request.dropStorage, targetState)) {
5497
5708
  await this.dropObjectStorage(singularTypeForRepo, request.name);
@@ -5550,18 +5761,8 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
5550
5761
  await this.dropObjectStorage(PLURAL_TO_SINGULAR3[request.type] ?? request.type, request.name);
5551
5762
  }
5552
5763
  }
5553
- if (this.environmentId === void 0) {
5554
- try {
5555
- const services = this.getServicesRegistry?.();
5556
- const metadataService = services?.get("metadata");
5557
- if (metadataService && typeof metadataService.get === "function") {
5558
- const artifactItem = await metadataService.get(request.type, request.name);
5559
- if (artifactItem !== void 0) {
5560
- this.engine.registry.registerItem(request.type, artifactItem, "name");
5561
- }
5562
- }
5563
- } catch {
5564
- }
5764
+ if (request.state !== "draft") {
5765
+ await this.restoreArtifactRegistryView(request.type, request.name);
5565
5766
  }
5566
5767
  return {
5567
5768
  success: true,
@@ -5599,7 +5800,12 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
5599
5800
  if (normalizedType === "object") {
5600
5801
  this.engine.registry.registerObject(data, record.packageId || "sys_metadata");
5601
5802
  } else {
5602
- this.engine.registry.registerItem(normalizedType, data, "name");
5803
+ const artifact = this.lookupArtifactItem(normalizedType, data?.name);
5804
+ this.engine.registry.registerItem(
5805
+ normalizedType,
5806
+ mergeArtifactProtection(data, artifact),
5807
+ "name"
5808
+ );
5603
5809
  }
5604
5810
  loaded++;
5605
5811
  } catch (e) {
@@ -7566,7 +7772,9 @@ var _ObjectQL = class _ObjectQL {
7566
7772
  "mappings",
7567
7773
  "analyticsCubes",
7568
7774
  // Integration Protocol
7569
- "connectors"
7775
+ "connectors",
7776
+ // System Protocol — package documentation (ADR-0046); inert data
7777
+ "docs"
7570
7778
  ];
7571
7779
  for (const key of metadataArrayKeys) {
7572
7780
  const items = manifest[key];
@@ -7702,7 +7910,8 @@ var _ObjectQL = class _ObjectQL {
7702
7910
  "hooks",
7703
7911
  "mappings",
7704
7912
  "analyticsCubes",
7705
- "connectors"
7913
+ "connectors",
7914
+ "docs"
7706
7915
  ];
7707
7916
  for (const key of metadataArrayKeys) {
7708
7917
  const items = plugin[key];
@@ -9231,10 +9440,11 @@ var MetadataFacade = class {
9231
9440
  */
9232
9441
  async register(type, name, data) {
9233
9442
  const definition = typeof data === "object" && data !== null ? { ...data, name: data.name ?? name } : data;
9443
+ const packageId = definition?._packageId;
9234
9444
  if (type === "object") {
9235
- this.registry.registerItem(type, definition, "name");
9445
+ this.registry.registerItem(type, definition, "name", packageId);
9236
9446
  } else {
9237
- this.registry.registerItem(type, definition, definition.id ? "id" : "name");
9447
+ this.registry.registerItem(type, definition, definition.id ? "id" : "name", packageId);
9238
9448
  }
9239
9449
  }
9240
9450
  /**
@@ -9908,7 +10118,7 @@ var ObjectQLPlugin = class {
9908
10118
  return;
9909
10119
  }
9910
10120
  if (this.ql?.registry?.registerItem) {
9911
- this.ql.registry.registerItem(type, item, keyField);
10121
+ this.ql.registry.registerItem(type, item, keyField, item._packageId);
9912
10122
  }
9913
10123
  });
9914
10124
  if (type === "hook" && this.ql && typeof this.ql.bindHooks === "function") {