@objectstack/objectql 7.7.0 → 7.9.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.js CHANGED
@@ -1288,6 +1288,30 @@ var SysMetadataRepository = class {
1288
1288
  yield header;
1289
1289
  }
1290
1290
  }
1291
+ /**
1292
+ * List pending DRAFT rows (ADR-0033) for this org, optionally narrowed by
1293
+ * `type` and/or `packageId`. Unlike {@link list} (which is hard-scoped to
1294
+ * `state='active'`), this reads `state='draft'` so the console can surface
1295
+ * what an AI authored but a human hasn't published yet. Returns a light
1296
+ * header projection (no body) suitable for a "pending changes" list.
1297
+ */
1298
+ async listDrafts(filter) {
1299
+ this.assertOpen();
1300
+ const where = {
1301
+ organization_id: this.organizationId,
1302
+ state: "draft"
1303
+ };
1304
+ if (filter?.type) where.type = filter.type;
1305
+ if (filter?.packageId) where.package_id = filter.packageId;
1306
+ const rows = await this.engine.find("sys_metadata", { where });
1307
+ return rows.map((row) => ({
1308
+ type: row.type,
1309
+ name: row.name,
1310
+ packageId: row.package_id ?? null,
1311
+ updatedAt: row.updated_at ?? row.created_at ?? null,
1312
+ updatedBy: row.updated_by ?? row.created_by ?? null
1313
+ }));
1314
+ }
1291
1315
  /**
1292
1316
  * Yield every history event for `(org, type?, name?)` from the
1293
1317
  * durable log, ordered by per-(type,name) `version` ascending. When
@@ -1797,6 +1821,13 @@ function resolveOverlaySchema(type, _item) {
1797
1821
  const singular = import_shared4.PLURAL_TO_SINGULAR[type] ?? type;
1798
1822
  return (0, import_kernel4.getMetadataTypeSchema)(singular) ?? null;
1799
1823
  }
1824
+ function normalizeViewMetadata(type, item, saveName) {
1825
+ const singular = import_shared4.PLURAL_TO_SINGULAR[type] ?? type;
1826
+ if (singular !== "view") return item;
1827
+ if (!item || typeof item !== "object" || Array.isArray(item)) return item;
1828
+ const it = item;
1829
+ return it.name ? it : { ...it, name: saveName };
1830
+ }
1800
1831
  function mergeArtifactProtection(item, artifactItem) {
1801
1832
  if (item === void 0 || item === null) return item;
1802
1833
  if (artifactItem === void 0 || artifactItem === null) return item;
@@ -2419,6 +2450,44 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2419
2450
  }
2420
2451
  } catch {
2421
2452
  }
2453
+ if (request.previewDrafts) {
2454
+ try {
2455
+ const orgId = request.organizationId;
2456
+ const queryDrafts = async (oid) => {
2457
+ const whereClause = { type: request.type, state: "draft", organization_id: oid };
2458
+ if (packageId) whereClause.package_id = packageId;
2459
+ let rs = await this.engine.find("sys_metadata", { where: whereClause });
2460
+ if (!rs || rs.length === 0) {
2461
+ const alt = import_shared4.PLURAL_TO_SINGULAR[request.type] ?? import_shared4.SINGULAR_TO_PLURAL[request.type];
2462
+ if (alt) {
2463
+ const altWhere = { type: alt, state: "draft", organization_id: oid };
2464
+ if (packageId) altWhere.package_id = packageId;
2465
+ rs = await this.engine.find("sys_metadata", { where: altWhere });
2466
+ }
2467
+ }
2468
+ return rs ?? [];
2469
+ };
2470
+ const draftRecords = [...await queryDrafts(null), ...orgId ? await queryDrafts(orgId) : []];
2471
+ if (draftRecords.length > 0) {
2472
+ const byName = /* @__PURE__ */ new Map();
2473
+ for (const existing of items) {
2474
+ const entry = existing;
2475
+ if (entry && typeof entry === "object" && "name" in entry) byName.set(entry.name, entry);
2476
+ }
2477
+ for (const record of draftRecords) {
2478
+ const data = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
2479
+ if (data && typeof data === "object" && "name" in data) {
2480
+ const recPkg = record.package_id ?? void 0;
2481
+ if (recPkg && data._packageId === void 0) data._packageId = recPkg;
2482
+ data._draft = true;
2483
+ byName.set(data.name, data);
2484
+ }
2485
+ }
2486
+ items = Array.from(byName.values());
2487
+ }
2488
+ } catch {
2489
+ }
2490
+ }
2422
2491
  try {
2423
2492
  const services = this.getServicesRegistry?.();
2424
2493
  const metadataService = services?.get("metadata");
@@ -2477,6 +2546,34 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2477
2546
  let item;
2478
2547
  const orgId = request.organizationId;
2479
2548
  const readState = request.state === "draft" ? "draft" : "active";
2549
+ if (request.previewDrafts && readState !== "draft") {
2550
+ try {
2551
+ const findDraft = async (oid) => {
2552
+ const rec = await this.engine.findOne("sys_metadata", {
2553
+ where: { type: request.type, name: request.name, state: "draft", organization_id: oid }
2554
+ });
2555
+ if (rec) return rec;
2556
+ const alt = import_shared4.PLURAL_TO_SINGULAR[request.type] ?? import_shared4.SINGULAR_TO_PLURAL[request.type];
2557
+ if (alt) {
2558
+ return await this.engine.findOne("sys_metadata", {
2559
+ where: { type: alt, name: request.name, state: "draft", organization_id: oid }
2560
+ });
2561
+ }
2562
+ return void 0;
2563
+ };
2564
+ const draftRec = (orgId ? await findDraft(orgId) : void 0) ?? await findDraft(null);
2565
+ if (draftRec) {
2566
+ const draftItem = typeof draftRec.metadata === "string" ? JSON.parse(draftRec.metadata) : draftRec.metadata;
2567
+ if (draftItem && typeof draftItem === "object") {
2568
+ const recPkg = draftRec.package_id ?? void 0;
2569
+ if (recPkg && draftItem._packageId === void 0) draftItem._packageId = recPkg;
2570
+ draftItem._draft = true;
2571
+ }
2572
+ return { type: request.type, name: request.name, item: decorateMetadataItem(request.type, draftItem) };
2573
+ }
2574
+ } catch {
2575
+ }
2576
+ }
2480
2577
  try {
2481
2578
  const findOverlay = async (oid) => {
2482
2579
  const where = {
@@ -3833,6 +3930,55 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3833
3930
  );
3834
3931
  }
3835
3932
  }
3933
+ /**
3934
+ * Ensure a just-PUBLISHED object's physical table exists so it is usable
3935
+ * for data CRUD immediately — without a server restart. Registering the
3936
+ * object (above) only updates the in-memory registry; the table is created
3937
+ * by the driver's schema sync, which otherwise only runs at boot. Without
3938
+ * this, inserting into a freshly-published object fails with "no such
3939
+ * table" (surfaced as `object_not_found`) until the next restart.
3940
+ * Best-effort + non-fatal: drivers without DDL (or read-only datasources)
3941
+ * simply no-op, and a sync failure must not abort the publish.
3942
+ */
3943
+ async ensureObjectStorage(type, name) {
3944
+ if (type !== "object" && type !== "objects") return;
3945
+ try {
3946
+ await this.engine.syncObjectSchema(name);
3947
+ } catch (err) {
3948
+ console.warn(`[Protocol] table sync failed for object '${name}': ${err?.message ?? err}`);
3949
+ }
3950
+ }
3951
+ /**
3952
+ * Inverse of {@link ensureObjectStorage}: drop an object's physical table.
3953
+ * DESTRUCTIVE — deletes the table and all its rows. Only invoked when a
3954
+ * delete explicitly opts into storage teardown (see {@link deleteMetaItem}'s
3955
+ * `dropStorage`), so publishing an object solely to preview it can be undone
3956
+ * without leaving an orphan table. Best-effort: a failure is logged, not
3957
+ * thrown — the metadata delete already succeeded, and a stray table is
3958
+ * reclaimed by the next sync/drop rather than blocking the delete.
3959
+ */
3960
+ async dropObjectStorage(type, name) {
3961
+ if (type !== "object" && type !== "objects") return;
3962
+ try {
3963
+ await this.engine.dropObjectSchema(name);
3964
+ } catch (err) {
3965
+ console.warn(`[Protocol] table drop failed for object '${name}': ${err?.message ?? err}`);
3966
+ }
3967
+ }
3968
+ /**
3969
+ * Guard for storage teardown on delete. Drops a physical table only when
3970
+ * the caller opted in AND it is safe: object types only (others have no
3971
+ * table), active state only (drafts were never materialised), and never a
3972
+ * `sys_`-prefixed platform table.
3973
+ */
3974
+ shouldDropStorage(type, name, dropStorage, state) {
3975
+ if (!dropStorage) return false;
3976
+ const singular = import_shared4.PLURAL_TO_SINGULAR[type] ?? type;
3977
+ if (singular !== "object") return false;
3978
+ if (state !== "active") return false;
3979
+ if (name.startsWith("sys_")) return false;
3980
+ return true;
3981
+ }
3836
3982
  async saveMetaItem(request) {
3837
3983
  if (!request.item) {
3838
3984
  throw new Error("Item data is required");
@@ -3906,6 +4052,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3906
4052
  throw err;
3907
4053
  }
3908
4054
  }
4055
+ request.item = normalizeViewMetadata(request.type, request.item, request.name);
3909
4056
  {
3910
4057
  const schema = resolveOverlaySchema(request.type, request.item);
3911
4058
  if (schema) {
@@ -3960,6 +4107,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3960
4107
  });
3961
4108
  if (mode === "publish") {
3962
4109
  this.applyObjectRegistryMutation(request);
4110
+ await this.ensureObjectStorage(request.type, request.name);
3963
4111
  }
3964
4112
  await this.recordMetadataAudit({
3965
4113
  type: request.type,
@@ -4130,6 +4278,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
4130
4278
  name: request.name,
4131
4279
  item: result.item.body
4132
4280
  });
4281
+ await this.ensureObjectStorage(request.type, request.name);
4133
4282
  return {
4134
4283
  success: true,
4135
4284
  version: result.version,
@@ -4150,6 +4299,160 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
4150
4299
  throw err;
4151
4300
  }
4152
4301
  }
4302
+ /**
4303
+ * List pending DRAFT metadata (ADR-0033) for the org, optionally narrowed
4304
+ * by `packageId` and/or `type`. The list reads of `getMetaItems` only see
4305
+ * the ACTIVE registry; this exposes what an AI authored but a human hasn't
4306
+ * published yet, so the console can show a "pending changes" surface and a
4307
+ * just-built app package isn't displayed as empty. No body is returned.
4308
+ */
4309
+ async listDrafts(request) {
4310
+ await this.ensureOverlayIndex();
4311
+ const orgId = request?.organizationId ?? null;
4312
+ const repo = this.getOverlayRepo(orgId);
4313
+ const drafts = await repo.listDrafts({
4314
+ ...request?.type ? { type: import_shared4.PLURAL_TO_SINGULAR[request.type] ?? request.type } : {},
4315
+ ...request?.packageId ? { packageId: request.packageId } : {}
4316
+ });
4317
+ return { drafts };
4318
+ }
4319
+ /**
4320
+ * Publish every pending DRAFT bound to a package in one shot (ADR-0033) —
4321
+ * the "publish whole app" action. Promotes each draft→active by reusing the
4322
+ * per-item {@link publishMetaItem} primitive (which runs the overridable /
4323
+ * lock guards and refreshes the runtime registry), so this needs NO
4324
+ * `metadata` service (unlike `MetadataService.publishPackage`, which reads
4325
+ * the in-memory registry and 503s when that service is absent). Per-item
4326
+ * failures are collected and do NOT abort the rest.
4327
+ */
4328
+ async publishPackageDrafts(request) {
4329
+ await this.ensureOverlayIndex();
4330
+ const orgId = request.organizationId ?? null;
4331
+ const repo = this.getOverlayRepo(orgId);
4332
+ const drafts = await repo.listDrafts({ packageId: request.packageId });
4333
+ const published = [];
4334
+ const failed = [];
4335
+ for (const d of drafts) {
4336
+ try {
4337
+ const r = await this.publishMetaItem({
4338
+ type: d.type,
4339
+ name: d.name,
4340
+ ...request.organizationId ? { organizationId: request.organizationId } : {},
4341
+ ...request.actor ? { actor: request.actor } : {},
4342
+ message: `publish app package '${request.packageId}'`
4343
+ });
4344
+ published.push({ type: d.type, name: d.name, version: r.version });
4345
+ } catch (e) {
4346
+ failed.push({
4347
+ type: d.type,
4348
+ name: d.name,
4349
+ error: e?.message ?? "publish failed",
4350
+ ...e?.code ? { code: e.code } : {}
4351
+ });
4352
+ }
4353
+ }
4354
+ return {
4355
+ success: failed.length === 0 && published.length > 0,
4356
+ publishedCount: published.length,
4357
+ failedCount: failed.length,
4358
+ published,
4359
+ failed
4360
+ };
4361
+ }
4362
+ /**
4363
+ * Discard every pending DRAFT bound to a package — the NON-destructive
4364
+ * inverse of {@link publishPackageDrafts}. Drops only `state='draft'` rows
4365
+ * (via the per-item delete primitive), reverting the package to its last
4366
+ * published baseline; active/published metadata and physical tables are
4367
+ * left untouched.
4368
+ *
4369
+ * Use case: "I edited this app for a while and it turned out worse than
4370
+ * before — abandon all my changes." Routes through the sys_metadata path
4371
+ * (no metadata-service dependency, unlike `POST /packages/:id/revert`).
4372
+ */
4373
+ async discardPackageDrafts(request) {
4374
+ await this.ensureOverlayIndex();
4375
+ const orgId = request.organizationId ?? null;
4376
+ const repo = this.getOverlayRepo(orgId);
4377
+ const drafts = await repo.listDrafts({ packageId: request.packageId });
4378
+ const discarded = [];
4379
+ const failed = [];
4380
+ for (const d of drafts) {
4381
+ try {
4382
+ await this.deleteMetaItem({
4383
+ type: d.type,
4384
+ name: d.name,
4385
+ state: "draft",
4386
+ ...request.organizationId ? { organizationId: request.organizationId } : {},
4387
+ ...request.actor ? { actor: request.actor } : {}
4388
+ });
4389
+ discarded.push({ type: d.type, name: d.name });
4390
+ } catch (e) {
4391
+ failed.push({
4392
+ type: d.type,
4393
+ name: d.name,
4394
+ error: e?.message ?? "discard failed",
4395
+ ...e?.code ? { code: e.code } : {}
4396
+ });
4397
+ }
4398
+ }
4399
+ return {
4400
+ success: failed.length === 0 && discarded.length > 0,
4401
+ discardedCount: discarded.length,
4402
+ failedCount: failed.length,
4403
+ discarded,
4404
+ failed
4405
+ };
4406
+ }
4407
+ /**
4408
+ * Delete an ENTIRE package: every `sys_metadata` row bound to it (active
4409
+ * AND draft) and — by default — the physical table of each object it
4410
+ * defined. DESTRUCTIVE: removes the app and its data. Use case: "I don't
4411
+ * want this package anymore."
4412
+ *
4413
+ * Set `keepData: true` to remove the metadata but preserve object tables.
4414
+ * The `sys_`-table guard in {@link deleteMetaItem} still applies, so
4415
+ * platform storage is never dropped. Drafts are removed before active rows
4416
+ * so each object's table is torn down once. Per-item failures are collected
4417
+ * without aborting the rest.
4418
+ */
4419
+ async deletePackage(request) {
4420
+ const where = { package_id: request.packageId };
4421
+ if (request.organizationId) where.organization_id = request.organizationId;
4422
+ const rows = await this.engine.find("sys_metadata", { where });
4423
+ const dropStorage = request.keepData !== true;
4424
+ const ordered = [...rows].sort((a, b) => (a.state === "draft" ? 0 : 1) - (b.state === "draft" ? 0 : 1));
4425
+ const deleted = [];
4426
+ const failed = [];
4427
+ for (const row of ordered) {
4428
+ const state = row.state === "draft" ? "draft" : "active";
4429
+ try {
4430
+ await this.deleteMetaItem({
4431
+ type: row.type,
4432
+ name: row.name,
4433
+ state,
4434
+ ...row.organization_id ? { organizationId: row.organization_id } : {},
4435
+ ...request.actor ? { actor: request.actor } : {},
4436
+ ...dropStorage ? { dropStorage: true } : {}
4437
+ });
4438
+ deleted.push({ type: row.type, name: row.name, state });
4439
+ } catch (e) {
4440
+ failed.push({
4441
+ type: row.type,
4442
+ name: row.name,
4443
+ error: e?.message ?? "delete failed",
4444
+ ...e?.code ? { code: e.code } : {}
4445
+ });
4446
+ }
4447
+ }
4448
+ return {
4449
+ success: failed.length === 0 && deleted.length > 0,
4450
+ deletedCount: deleted.length,
4451
+ failedCount: failed.length,
4452
+ deleted,
4453
+ failed
4454
+ };
4455
+ }
4153
4456
  /**
4154
4457
  * Restore the body recorded at history `toVersion` as the new
4155
4458
  * live row. Writes a history event with `op='revert'`. 404
@@ -4382,6 +4685,9 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
4382
4685
  } catch {
4383
4686
  }
4384
4687
  }
4688
+ if (this.shouldDropStorage(request.type, request.name, request.dropStorage, targetState)) {
4689
+ await this.dropObjectStorage(singularTypeForRepo, request.name);
4690
+ }
4385
4691
  await this.recordMetadataAudit({
4386
4692
  type: request.type,
4387
4693
  name: request.name,
@@ -4430,6 +4736,12 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
4430
4736
  };
4431
4737
  }
4432
4738
  await this.engine.delete("sys_metadata", { where: { id: existing.id } });
4739
+ {
4740
+ const targetState = request.state === "draft" ? "draft" : "active";
4741
+ if (this.shouldDropStorage(request.type, request.name, request.dropStorage, targetState)) {
4742
+ await this.dropObjectStorage(import_shared4.PLURAL_TO_SINGULAR[request.type] ?? request.type, request.name);
4743
+ }
4744
+ }
4433
4745
  if (this.environmentId === void 0) {
4434
4746
  try {
4435
4747
  const services = this.getServicesRegistry?.();
@@ -4687,6 +4999,39 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
4687
4999
  const unsubscribed = await svc.unsubscribe(request.object, request.recordId, "current_user");
4688
5000
  return { success: true, data: { object: request.object, recordId: request.recordId, unsubscribed } };
4689
5001
  }
5002
+ /**
5003
+ * Install a package from a manifest — the single canonical write primitive
5004
+ * for the package subsystem (ADR-0033 consolidation).
5005
+ *
5006
+ * It writes BOTH stores that the runtime keeps for packages, so a package
5007
+ * surfaces consistently no matter which read path is used:
5008
+ * 1. the in-memory `SchemaRegistry` (what the dispatcher's
5009
+ * `/api/v1/packages` list/detail and `getMetaItems({type:'package'})`
5010
+ * read — i.e. what Studio's package selector shows), and
5011
+ * 2. the durable `sys_packages` table via the optional `package` service
5012
+ * (so the package survives a restart; that service re-hydrates these
5013
+ * rows back into the registry on boot).
5014
+ *
5015
+ * The DB write is best-effort and non-fatal: when the `package` service is
5016
+ * absent (e.g. the `marketplace` capability is off) the package is still
5017
+ * registered in-memory and visible for the lifetime of the process.
5018
+ */
5019
+ async installPackage(request) {
5020
+ const manifest = request.manifest;
5021
+ const pkg = this.engine.registry.installPackage(manifest, request.settings);
5022
+ try {
5023
+ const services = this.getServicesRegistry?.();
5024
+ const pkgSvc = services?.get("package");
5025
+ if (pkgSvc?.publish && manifest?.version) {
5026
+ await pkgSvc.publish({ manifest, metadata: {} });
5027
+ }
5028
+ } catch (e) {
5029
+ console.warn(
5030
+ `[protocol.installPackage] sys_packages persist skipped for '${manifest?.id}': ${e?.message}`
5031
+ );
5032
+ }
5033
+ return { package: pkg, message: `Installed package: ${manifest?.id}` };
5034
+ }
4690
5035
  };
4691
5036
  /**
4692
5037
  * Metadata types that are customer-overridable via {@link saveMetaItem}/
@@ -7481,6 +7826,39 @@ var _ObjectQL = class _ObjectQL {
7481
7826
  }
7482
7827
  }
7483
7828
  }
7829
+ /**
7830
+ * Sync a SINGLE object's physical storage (create/alter its table) on
7831
+ * demand. Boot-time {@link syncSchemas} runs once at startup, so an object
7832
+ * that becomes live at runtime (e.g. publishing a drafted object) has a
7833
+ * registry entry but no table — data CRUD then fails with "no such table"
7834
+ * until the next restart. Calling this right after the object is registered
7835
+ * makes it immediately usable. Idempotent: the SQL driver only creates the
7836
+ * table when absent (and alters to add new columns).
7837
+ */
7838
+ async syncObjectSchema(objectName) {
7839
+ const obj = this._registry.getObject(objectName);
7840
+ if (!obj) return;
7841
+ const driver = this.getDriverForObject(objectName);
7842
+ if (!driver || typeof driver.syncSchema !== "function") return;
7843
+ const tableName = import_system2.StorageNameMapping.resolveTableName(obj);
7844
+ await driver.syncSchema(tableName, obj);
7845
+ }
7846
+ /**
7847
+ * Drop the physical storage (table/collection) backing an object — the
7848
+ * inverse of {@link syncObjectSchema}. DESTRUCTIVE: deletes all rows in the
7849
+ * table. Used by the protocol delete path when the caller explicitly opts
7850
+ * into storage teardown (e.g. discarding an object that was published only
7851
+ * to preview it). No-op when the object's driver does not expose `dropTable`.
7852
+ * Resolves the physical table name from the registered definition, falling
7853
+ * back to the bare name if the def was already removed.
7854
+ */
7855
+ async dropObjectSchema(objectName) {
7856
+ const obj = this._registry.getObject(objectName);
7857
+ const driver = this.getDriverForObject(objectName);
7858
+ if (!driver || typeof driver.dropTable !== "function") return;
7859
+ const tableName = import_system2.StorageNameMapping.resolveTableName(obj ?? { name: objectName });
7860
+ await driver.dropTable(tableName);
7861
+ }
7484
7862
  /**
7485
7863
  * Get a registered driver by datasource name.
7486
7864
  * Alias matching @objectql/core datasource() API.