@objectstack/objectql 7.1.0 → 7.2.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
@@ -454,7 +454,9 @@ var SchemaRegistry = class {
454
454
  const direct = collection.get(name);
455
455
  if (direct) return direct;
456
456
  for (const [key, item] of collection) {
457
- if (key.endsWith(`:${name}`)) return item;
457
+ if (key.endsWith(`:${name}`)) {
458
+ return item;
459
+ }
458
460
  }
459
461
  return void 0;
460
462
  }
@@ -1306,6 +1308,12 @@ import { parseFilterAST, isFilterAST } from "@objectstack/spec/data";
1306
1308
  import { PLURAL_TO_SINGULAR as PLURAL_TO_SINGULAR3, SINGULAR_TO_PLURAL as SINGULAR_TO_PLURAL2 } from "@objectstack/spec/shared";
1307
1309
  import { METADATA_FORM_REGISTRY } from "@objectstack/spec/system";
1308
1310
  import { DEFAULT_METADATA_TYPE_REGISTRY as DEFAULT_METADATA_TYPE_REGISTRY2, getMetadataTypeSchema as getMetadataTypeSchema2 } from "@objectstack/spec/kernel";
1311
+ import {
1312
+ extractProtection,
1313
+ evaluateLockForWrite,
1314
+ evaluateLockForDelete,
1315
+ resolveLockState
1316
+ } from "@objectstack/spec/kernel";
1309
1317
  import { z } from "zod";
1310
1318
 
1311
1319
  // src/metadata-diagnostics.ts
@@ -1553,6 +1561,19 @@ function resolveOverlaySchema(type, _item) {
1553
1561
  const singular = PLURAL_TO_SINGULAR3[type] ?? type;
1554
1562
  return getMetadataTypeSchema2(singular) ?? null;
1555
1563
  }
1564
+ function mergeArtifactProtection(item, artifactItem) {
1565
+ if (item === void 0 || item === null) return item;
1566
+ if (artifactItem === void 0 || artifactItem === null) return item;
1567
+ const a = artifactItem;
1568
+ if (typeof a !== "object") return item;
1569
+ const out = { ...item };
1570
+ if (a._lock !== void 0) out._lock = a._lock;
1571
+ if (a._lockReason !== void 0) out._lockReason = a._lockReason;
1572
+ if (a._packageId !== void 0) out._packageId = a._packageId;
1573
+ if (a._packageVersion !== void 0) out._packageVersion = a._packageVersion;
1574
+ if (a._provenance !== void 0) out._provenance = a._provenance;
1575
+ return out;
1576
+ }
1556
1577
  function simpleHash(str) {
1557
1578
  let hash = 0;
1558
1579
  for (let i = 0; i < str.length; i++) {
@@ -2044,6 +2065,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2044
2065
  const includeWarnings = request.severity === "warning";
2045
2066
  const targetTypes = request.type ? [request.type] : DEFAULT_METADATA_TYPE_REGISTRY2.filter((e) => getMetadataTypeSchema2(e.type)).map((e) => e.type);
2046
2067
  const entries = [];
2068
+ const stats = {};
2047
2069
  let scannedItems = 0;
2048
2070
  for (const t of targetTypes) {
2049
2071
  let listed;
@@ -2057,8 +2079,11 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2057
2079
  continue;
2058
2080
  }
2059
2081
  const items = Array.isArray(listed?.items) ? listed.items : Array.isArray(listed) ? listed : [];
2082
+ const pkgSet = /* @__PURE__ */ new Set();
2060
2083
  for (const item of items) {
2061
2084
  scannedItems += 1;
2085
+ const pkg = item?._packageId ?? null;
2086
+ if (pkg) pkgSet.add(pkg);
2062
2087
  const diag = item?._diagnostics ?? computeMetadataDiagnostics(t, item);
2063
2088
  if (!diag) continue;
2064
2089
  if (diag.valid && !includeWarnings) continue;
@@ -2069,12 +2094,14 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2069
2094
  diagnostics: diag
2070
2095
  });
2071
2096
  }
2097
+ stats[t] = { count: items.length, packages: [...pkgSet].sort() };
2072
2098
  }
2073
2099
  return {
2074
2100
  entries,
2075
2101
  total: entries.length,
2076
2102
  scannedTypes: targetTypes.length,
2077
- scannedItems
2103
+ scannedItems,
2104
+ stats
2078
2105
  };
2079
2106
  }
2080
2107
  async getMetaItems(request) {
@@ -2171,7 +2198,16 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2171
2198
  }
2172
2199
  return {
2173
2200
  type: request.type,
2174
- items: decorateMetadataItems(request.type, items)
2201
+ items: decorateMetadataItems(
2202
+ request.type,
2203
+ items.map((it) => {
2204
+ const a = this.lookupArtifactItem(
2205
+ request.type,
2206
+ it?.name
2207
+ );
2208
+ return mergeArtifactProtection(it, a);
2209
+ })
2210
+ )
2175
2211
  };
2176
2212
  }
2177
2213
  async getMetaItem(request) {
@@ -2245,10 +2281,26 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2245
2281
  if (alt) item = this.engine.registry.getItem(alt, request.name);
2246
2282
  }
2247
2283
  }
2284
+ const artifactItem = this.lookupArtifactItem(request.type, request.name);
2285
+ const decorated = decorateMetadataItem(
2286
+ request.type,
2287
+ mergeArtifactProtection(item, artifactItem)
2288
+ );
2289
+ const artifactBacked = this.isArtifactBacked(request.type, request.name);
2290
+ const lockState = resolveLockState(decorated, artifactBacked);
2248
2291
  return {
2249
2292
  type: request.type,
2250
2293
  name: request.name,
2251
- item: decorateMetadataItem(request.type, item)
2294
+ item: decorated,
2295
+ lock: lockState.lock,
2296
+ ...lockState.lockReason !== void 0 ? { lockReason: lockState.lockReason } : {},
2297
+ ...lockState.lockSource !== void 0 ? { lockSource: lockState.lockSource } : {},
2298
+ ...lockState.provenance !== void 0 ? { provenance: lockState.provenance } : {},
2299
+ ...lockState.packageId !== void 0 ? { packageId: lockState.packageId } : {},
2300
+ ...lockState.packageVersion !== void 0 ? { packageVersion: lockState.packageVersion } : {},
2301
+ editable: lockState.editable,
2302
+ deletable: lockState.deletable,
2303
+ resettable: lockState.resettable
2252
2304
  };
2253
2305
  }
2254
2306
  /**
@@ -2328,6 +2380,9 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2328
2380
  }
2329
2381
  const effective = overlay ?? code;
2330
2382
  const _diagnostics = effective !== null && effective !== void 0 ? computeMetadataDiagnostics(request.type, effective) : void 0;
2383
+ const artifactBacked = this.isArtifactBacked(request.type, request.name);
2384
+ const lockSource = code ?? overlay ?? {};
2385
+ const lockState = resolveLockState(lockSource, artifactBacked);
2331
2386
  return {
2332
2387
  type: request.type,
2333
2388
  name: request.name,
@@ -2335,9 +2390,69 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2335
2390
  overlay,
2336
2391
  overlayScope,
2337
2392
  effective,
2338
- ..._diagnostics ? { _diagnostics } : {}
2393
+ ..._diagnostics ? { _diagnostics } : {},
2394
+ lock: lockState.lock,
2395
+ ...lockState.lockReason !== void 0 ? { lockReason: lockState.lockReason } : {},
2396
+ ...lockState.lockSource !== void 0 ? { lockSource: lockState.lockSource } : {},
2397
+ ...lockState.provenance !== void 0 ? { provenance: lockState.provenance } : {},
2398
+ ...lockState.packageId !== void 0 ? { packageId: lockState.packageId } : {},
2399
+ ...lockState.packageVersion !== void 0 ? { packageVersion: lockState.packageVersion } : {},
2400
+ editable: lockState.editable,
2401
+ deletable: lockState.deletable,
2402
+ resettable: lockState.resettable
2339
2403
  };
2340
2404
  }
2405
+ /**
2406
+ * ADR-0010 §3.6 / Phase 4.1 — read the metadata-protection audit log
2407
+ * for a single item. Returns the most-recent rows of
2408
+ * `sys_metadata_audit` for this (type, name) tuple, sorted newest
2409
+ * first. Refused (`denied`) and forced (`forced`) writes both appear
2410
+ * here — they never reach the `history` endpoint, which only tracks
2411
+ * successful body snapshots.
2412
+ *
2413
+ * The table is provisioned by `platform-objects` and is the
2414
+ * compliance surface for the lock-enforcement story. When the
2415
+ * environment has not yet provisioned the table (legacy install
2416
+ * prior to ADR-0010) the call returns `{ events: [] }` instead of
2417
+ * raising, keeping the Studio tab harmless.
2418
+ */
2419
+ async auditMetaItem(request) {
2420
+ const singular = PLURAL_TO_SINGULAR3[request.type] ?? request.type;
2421
+ const limit = Math.min(
2422
+ Math.max(1, request.limit ?? 100),
2423
+ 500
2424
+ );
2425
+ try {
2426
+ const where = {
2427
+ type: singular,
2428
+ name: request.name
2429
+ };
2430
+ const rows = await this.engine.find("sys_metadata_audit", {
2431
+ where,
2432
+ orderBy: [{ field: "occurred_at", direction: "desc" }],
2433
+ limit
2434
+ });
2435
+ const events = (Array.isArray(rows) ? rows : []).map((r) => ({
2436
+ id: r.id,
2437
+ occurredAt: typeof r.occurred_at === "string" ? r.occurred_at : r.occurred_at instanceof Date ? r.occurred_at.toISOString() : String(r.occurred_at ?? ""),
2438
+ actor: String(r.actor ?? "system"),
2439
+ source: r.source ?? null,
2440
+ operation: r.operation,
2441
+ outcome: r.outcome,
2442
+ code: String(r.code ?? ""),
2443
+ lockState: r.lock_state ?? null,
2444
+ lockOverridden: Boolean(r.lock_overridden),
2445
+ requestId: r.request_id ?? null,
2446
+ note: r.note ?? null
2447
+ }));
2448
+ return { events };
2449
+ } catch (err) {
2450
+ console.warn(
2451
+ `[Protocol] auditMetaItem read failed for ${request.type}/${request.name}: ${err?.message ?? err}`
2452
+ );
2453
+ return { events: [] };
2454
+ }
2455
+ }
2341
2456
  async getUiView(request) {
2342
2457
  const schema = this.engine.registry.getObject(request.object);
2343
2458
  if (!schema) throw new Error(`Object ${request.object} not found`);
@@ -3273,6 +3388,158 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3273
3388
  if (!item || !item._packageId) return false;
3274
3389
  return item._packageId !== "sys_metadata";
3275
3390
  }
3391
+ // ───────────────────────────────────────────────────────────────────
3392
+ // ADR-0010 — metadata protection (Phase 1: L3 item-level lock)
3393
+ // ───────────────────────────────────────────────────────────────────
3394
+ /**
3395
+ * Look up an item from the artifact registry across both the requested
3396
+ * type and its singular/plural twin. Returns `undefined` when the
3397
+ * registry is unavailable or the item is not artifact-backed.
3398
+ */
3399
+ lookupArtifactItem(type, name) {
3400
+ const registry = this.engine?.registry;
3401
+ if (!registry || typeof registry.getItem !== "function") return void 0;
3402
+ const singular = PLURAL_TO_SINGULAR3[type] ?? type;
3403
+ return registry.getItem(singular, name) ?? registry.getItem(type, name);
3404
+ }
3405
+ /**
3406
+ * Resolve the effective `_lock` for an item by consulting the
3407
+ * artifact registry first, then the persisted overlay row. Artifact
3408
+ * always wins — by design, an overlay cannot loosen a packaged
3409
+ * lock (ADR-0010 §3.3).
3410
+ *
3411
+ * Returns `'none'` when nothing is locked, which is the common
3412
+ * case. Safe to call when `environmentId` is undefined (control-
3413
+ * plane bootstrap) — the lock check is only meaningful in tenant
3414
+ * scope and the caller is expected to also gate on `environmentId`.
3415
+ */
3416
+ async getEffectiveLock(type, name, organizationId) {
3417
+ const registry = this.engine?.registry;
3418
+ const singular = PLURAL_TO_SINGULAR3[type] ?? type;
3419
+ let artifactItem;
3420
+ if (registry && typeof registry.getItem === "function") {
3421
+ artifactItem = registry.getItem(singular, name) ?? registry.getItem(type, name);
3422
+ }
3423
+ if (artifactItem && artifactItem._packageId && artifactItem._packageId !== "sys_metadata") {
3424
+ const p = extractProtection(artifactItem);
3425
+ if (p.lock !== "none") {
3426
+ return { lock: p.lock, lockReason: p.lockReason, lockSource: "artifact" };
3427
+ }
3428
+ }
3429
+ try {
3430
+ const where = {
3431
+ type,
3432
+ name,
3433
+ state: "active",
3434
+ organization_id: organizationId ?? null
3435
+ };
3436
+ const row = await this.engine.findOne("sys_metadata", { where });
3437
+ if (row) {
3438
+ const body = typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata;
3439
+ const p = extractProtection(body);
3440
+ if (p.lock !== "none") {
3441
+ return { lock: p.lock, lockReason: p.lockReason, lockSource: "overlay" };
3442
+ }
3443
+ }
3444
+ } catch {
3445
+ }
3446
+ return { lock: "none", lockReason: void 0, lockSource: void 0 };
3447
+ }
3448
+ /**
3449
+ * Best-effort audit-row writer (ADR-0010 §3.6). Failures here are
3450
+ * logged but never block the underlying decision: an environment
3451
+ * without the audit table provisioned (legacy installs before this
3452
+ * ADR landed) still answers normal API calls, just without the
3453
+ * compliance trail. Phase 2 will make the audit table a hard
3454
+ * dependency.
3455
+ */
3456
+ async recordMetadataAudit(entry) {
3457
+ try {
3458
+ await this.engine.insert("sys_metadata_audit", {
3459
+ occurred_at: (/* @__PURE__ */ new Date()).toISOString(),
3460
+ actor: entry.actor ?? "system",
3461
+ source: entry.source ?? "protocol",
3462
+ type: PLURAL_TO_SINGULAR3[entry.type] ?? entry.type,
3463
+ name: entry.name,
3464
+ organization_id: entry.organizationId ?? null,
3465
+ operation: entry.operation,
3466
+ outcome: entry.outcome,
3467
+ code: entry.code,
3468
+ lock_state: entry.lockState ?? "none",
3469
+ lock_overridden: entry.lockOverridden ?? false,
3470
+ request_id: entry.requestId ?? null,
3471
+ note: entry.note ?? null
3472
+ });
3473
+ } catch (err) {
3474
+ console.warn(
3475
+ `[Protocol] sys_metadata_audit write failed for ${entry.type}/${entry.name}: ${err?.message ?? err}`
3476
+ );
3477
+ }
3478
+ }
3479
+ /**
3480
+ * Phase 1 L3 enforcement for write operations (save / publish /
3481
+ * rollback). Returns null on allow. Returns the structured `Error`
3482
+ * the caller should `throw` on deny — also records the denial in
3483
+ * the audit log so refused attempts are visible in compliance
3484
+ * reports (refused writes never reach sys_metadata_history).
3485
+ */
3486
+ async assertLockAllowsWrite(args) {
3487
+ if (this.environmentId === void 0) return null;
3488
+ const state = await this.getEffectiveLock(args.type, args.name, args.organizationId ?? null);
3489
+ const refusal = evaluateLockForWrite(state.lock);
3490
+ if (!refusal) return null;
3491
+ const reason = state.lockReason ?? refusal.reason;
3492
+ const err = new Error(
3493
+ `[item_locked] ${args.type}/${args.name} is locked (_lock=${state.lock}${state.lockSource ? `, source=${state.lockSource}` : ""}). ${reason} \u2014 See ADR-0010 \xA73.3.`
3494
+ );
3495
+ err.code = "item_locked";
3496
+ err.status = 403;
3497
+ err.lock = state.lock;
3498
+ err.lockReason = reason;
3499
+ await this.recordMetadataAudit({
3500
+ type: args.type,
3501
+ name: args.name,
3502
+ organizationId: args.organizationId ?? null,
3503
+ operation: args.operation,
3504
+ outcome: "denied",
3505
+ code: "item_locked",
3506
+ lockState: state.lock,
3507
+ actor: args.actor,
3508
+ source: args.source ?? `protocol.${args.operation}MetaItem`,
3509
+ requestId: args.requestId,
3510
+ note: reason
3511
+ });
3512
+ return err;
3513
+ }
3514
+ /** Counterpart of {@link assertLockAllowsWrite} for delete. */
3515
+ async assertLockAllowsDelete(args) {
3516
+ if (this.environmentId === void 0) return null;
3517
+ const state = await this.getEffectiveLock(args.type, args.name, args.organizationId ?? null);
3518
+ const refusal = evaluateLockForDelete(state.lock);
3519
+ if (!refusal) return null;
3520
+ const reason = state.lockReason ?? refusal.reason;
3521
+ const err = new Error(
3522
+ `[item_locked] ${args.type}/${args.name} is locked (_lock=${state.lock}${state.lockSource ? `, source=${state.lockSource}` : ""}). ${reason} \u2014 See ADR-0010 \xA73.3.`
3523
+ );
3524
+ err.code = "item_locked";
3525
+ err.status = 403;
3526
+ err.lock = state.lock;
3527
+ err.lockReason = reason;
3528
+ await this.recordMetadataAudit({
3529
+ type: args.type,
3530
+ name: args.name,
3531
+ organizationId: args.organizationId ?? null,
3532
+ operation: "delete",
3533
+ outcome: "denied",
3534
+ code: "item_locked",
3535
+ lockState: state.lock,
3536
+ actor: args.actor,
3537
+ source: args.source ?? "protocol.deleteMetaItem",
3538
+ requestId: args.requestId,
3539
+ note: reason
3540
+ });
3541
+ return err;
3542
+ }
3276
3543
  /**
3277
3544
  * Mirror an object-type overlay write into the in-memory engine
3278
3545
  * registry so subsequent CRUD finds the new schema. Idempotent and
@@ -3319,6 +3586,15 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3319
3586
  err.status = 403;
3320
3587
  throw err;
3321
3588
  }
3589
+ const lockErr = await this.assertLockAllowsWrite({
3590
+ type: request.type,
3591
+ name: request.name,
3592
+ ...request.organizationId ? { organizationId: request.organizationId } : {},
3593
+ operation: "save",
3594
+ ...request.actor ? { actor: request.actor } : {},
3595
+ source: "protocol.saveMetaItem"
3596
+ });
3597
+ if (lockErr) throw lockErr;
3322
3598
  }
3323
3599
  const singularType = PLURAL_TO_SINGULAR3[request.type] ?? request.type;
3324
3600
  if (!request.force && (singularType === "object" || singularType === "field")) {
@@ -3400,6 +3676,17 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3400
3676
  if (mode === "publish") {
3401
3677
  this.applyObjectRegistryMutation(request);
3402
3678
  }
3679
+ await this.recordMetadataAudit({
3680
+ type: request.type,
3681
+ name: request.name,
3682
+ organizationId: orgId,
3683
+ operation: "save",
3684
+ outcome: "allowed",
3685
+ code: "ok",
3686
+ ...request.actor ? { actor: request.actor } : {},
3687
+ source: "protocol.saveMetaItem",
3688
+ note: mode === "draft" ? "draft" : "active"
3689
+ });
3403
3690
  return {
3404
3691
  success: true,
3405
3692
  version: result.version,
@@ -3522,6 +3809,15 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3522
3809
  err.status = 403;
3523
3810
  throw err;
3524
3811
  }
3812
+ const _publishLockErr = await this.assertLockAllowsWrite({
3813
+ type: request.type,
3814
+ name: request.name,
3815
+ ...request.organizationId ? { organizationId: request.organizationId } : {},
3816
+ operation: "publish",
3817
+ ...request.actor ? { actor: request.actor } : {},
3818
+ source: "protocol.publishMetaItem"
3819
+ });
3820
+ if (_publishLockErr) throw _publishLockErr;
3525
3821
  await this.ensureOverlayIndex();
3526
3822
  const orgId = request.organizationId ?? null;
3527
3823
  const repo = this.getOverlayRepo(orgId);
@@ -3589,6 +3885,15 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3589
3885
  err.status = 403;
3590
3886
  throw err;
3591
3887
  }
3888
+ const _rollbackLockErr = await this.assertLockAllowsWrite({
3889
+ type: request.type,
3890
+ name: request.name,
3891
+ ...request.organizationId ? { organizationId: request.organizationId } : {},
3892
+ operation: "rollback",
3893
+ ...request.actor ? { actor: request.actor } : {},
3894
+ source: "protocol.rollbackMetaItem"
3895
+ });
3896
+ if (_rollbackLockErr) throw _rollbackLockErr;
3592
3897
  await this.ensureOverlayIndex();
3593
3898
  const orgId = request.organizationId ?? null;
3594
3899
  const repo = this.getOverlayRepo(orgId);
@@ -3735,6 +4040,14 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3735
4040
  err.status = 403;
3736
4041
  throw err;
3737
4042
  }
4043
+ const lockErr = await this.assertLockAllowsDelete({
4044
+ type: request.type,
4045
+ name: request.name,
4046
+ ...request.organizationId ? { organizationId: request.organizationId } : {},
4047
+ ...request.actor ? { actor: request.actor } : {},
4048
+ source: "protocol.deleteMetaItem"
4049
+ });
4050
+ if (lockErr) throw lockErr;
3738
4051
  }
3739
4052
  const singularTypeForRepo = PLURAL_TO_SINGULAR3[request.type] ?? request.type;
3740
4053
  const overlayAllowedForRepoDel = _ObjectStackProtocolImplementation.isOverlayAllowed(singularTypeForRepo);
@@ -3779,6 +4092,17 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3779
4092
  } catch {
3780
4093
  }
3781
4094
  }
4095
+ await this.recordMetadataAudit({
4096
+ type: request.type,
4097
+ name: request.name,
4098
+ organizationId: orgId,
4099
+ operation: "delete",
4100
+ outcome: "allowed",
4101
+ code: "ok",
4102
+ ...request.actor ? { actor: request.actor } : {},
4103
+ source: "protocol.deleteMetaItem",
4104
+ note: targetState
4105
+ });
3782
4106
  return {
3783
4107
  success: true,
3784
4108
  reset: true,