@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.mjs CHANGED
@@ -1219,6 +1219,30 @@ var SysMetadataRepository = class {
1219
1219
  yield header;
1220
1220
  }
1221
1221
  }
1222
+ /**
1223
+ * List pending DRAFT rows (ADR-0033) for this org, optionally narrowed by
1224
+ * `type` and/or `packageId`. Unlike {@link list} (which is hard-scoped to
1225
+ * `state='active'`), this reads `state='draft'` so the console can surface
1226
+ * what an AI authored but a human hasn't published yet. Returns a light
1227
+ * header projection (no body) suitable for a "pending changes" list.
1228
+ */
1229
+ async listDrafts(filter) {
1230
+ this.assertOpen();
1231
+ const where = {
1232
+ organization_id: this.organizationId,
1233
+ state: "draft"
1234
+ };
1235
+ if (filter?.type) where.type = filter.type;
1236
+ if (filter?.packageId) where.package_id = filter.packageId;
1237
+ const rows = await this.engine.find("sys_metadata", { where });
1238
+ return rows.map((row) => ({
1239
+ type: row.type,
1240
+ name: row.name,
1241
+ packageId: row.package_id ?? null,
1242
+ updatedAt: row.updated_at ?? row.created_at ?? null,
1243
+ updatedBy: row.updated_by ?? row.created_by ?? null
1244
+ }));
1245
+ }
1222
1246
  /**
1223
1247
  * Yield every history event for `(org, type?, name?)` from the
1224
1248
  * durable log, ordered by per-(type,name) `version` ascending. When
@@ -1733,6 +1757,13 @@ function resolveOverlaySchema(type, _item) {
1733
1757
  const singular = PLURAL_TO_SINGULAR3[type] ?? type;
1734
1758
  return getMetadataTypeSchema2(singular) ?? null;
1735
1759
  }
1760
+ function normalizeViewMetadata(type, item, saveName) {
1761
+ const singular = PLURAL_TO_SINGULAR3[type] ?? type;
1762
+ if (singular !== "view") return item;
1763
+ if (!item || typeof item !== "object" || Array.isArray(item)) return item;
1764
+ const it = item;
1765
+ return it.name ? it : { ...it, name: saveName };
1766
+ }
1736
1767
  function mergeArtifactProtection(item, artifactItem) {
1737
1768
  if (item === void 0 || item === null) return item;
1738
1769
  if (artifactItem === void 0 || artifactItem === null) return item;
@@ -2355,6 +2386,44 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2355
2386
  }
2356
2387
  } catch {
2357
2388
  }
2389
+ if (request.previewDrafts) {
2390
+ try {
2391
+ const orgId = request.organizationId;
2392
+ const queryDrafts = async (oid) => {
2393
+ const whereClause = { type: request.type, state: "draft", organization_id: oid };
2394
+ if (packageId) whereClause.package_id = packageId;
2395
+ let rs = await this.engine.find("sys_metadata", { where: whereClause });
2396
+ if (!rs || rs.length === 0) {
2397
+ const alt = PLURAL_TO_SINGULAR3[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
2398
+ if (alt) {
2399
+ const altWhere = { type: alt, state: "draft", organization_id: oid };
2400
+ if (packageId) altWhere.package_id = packageId;
2401
+ rs = await this.engine.find("sys_metadata", { where: altWhere });
2402
+ }
2403
+ }
2404
+ return rs ?? [];
2405
+ };
2406
+ const draftRecords = [...await queryDrafts(null), ...orgId ? await queryDrafts(orgId) : []];
2407
+ if (draftRecords.length > 0) {
2408
+ const byName = /* @__PURE__ */ new Map();
2409
+ for (const existing of items) {
2410
+ const entry = existing;
2411
+ if (entry && typeof entry === "object" && "name" in entry) byName.set(entry.name, entry);
2412
+ }
2413
+ for (const record of draftRecords) {
2414
+ const data = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
2415
+ if (data && typeof data === "object" && "name" in data) {
2416
+ const recPkg = record.package_id ?? void 0;
2417
+ if (recPkg && data._packageId === void 0) data._packageId = recPkg;
2418
+ data._draft = true;
2419
+ byName.set(data.name, data);
2420
+ }
2421
+ }
2422
+ items = Array.from(byName.values());
2423
+ }
2424
+ } catch {
2425
+ }
2426
+ }
2358
2427
  try {
2359
2428
  const services = this.getServicesRegistry?.();
2360
2429
  const metadataService = services?.get("metadata");
@@ -2413,6 +2482,34 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2413
2482
  let item;
2414
2483
  const orgId = request.organizationId;
2415
2484
  const readState = request.state === "draft" ? "draft" : "active";
2485
+ if (request.previewDrafts && readState !== "draft") {
2486
+ try {
2487
+ const findDraft = async (oid) => {
2488
+ const rec = await this.engine.findOne("sys_metadata", {
2489
+ where: { type: request.type, name: request.name, state: "draft", organization_id: oid }
2490
+ });
2491
+ if (rec) return rec;
2492
+ const alt = PLURAL_TO_SINGULAR3[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
2493
+ if (alt) {
2494
+ return await this.engine.findOne("sys_metadata", {
2495
+ where: { type: alt, name: request.name, state: "draft", organization_id: oid }
2496
+ });
2497
+ }
2498
+ return void 0;
2499
+ };
2500
+ const draftRec = (orgId ? await findDraft(orgId) : void 0) ?? await findDraft(null);
2501
+ if (draftRec) {
2502
+ const draftItem = typeof draftRec.metadata === "string" ? JSON.parse(draftRec.metadata) : draftRec.metadata;
2503
+ if (draftItem && typeof draftItem === "object") {
2504
+ const recPkg = draftRec.package_id ?? void 0;
2505
+ if (recPkg && draftItem._packageId === void 0) draftItem._packageId = recPkg;
2506
+ draftItem._draft = true;
2507
+ }
2508
+ return { type: request.type, name: request.name, item: decorateMetadataItem(request.type, draftItem) };
2509
+ }
2510
+ } catch {
2511
+ }
2512
+ }
2416
2513
  try {
2417
2514
  const findOverlay = async (oid) => {
2418
2515
  const where = {
@@ -3769,6 +3866,55 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3769
3866
  );
3770
3867
  }
3771
3868
  }
3869
+ /**
3870
+ * Ensure a just-PUBLISHED object's physical table exists so it is usable
3871
+ * for data CRUD immediately — without a server restart. Registering the
3872
+ * object (above) only updates the in-memory registry; the table is created
3873
+ * by the driver's schema sync, which otherwise only runs at boot. Without
3874
+ * this, inserting into a freshly-published object fails with "no such
3875
+ * table" (surfaced as `object_not_found`) until the next restart.
3876
+ * Best-effort + non-fatal: drivers without DDL (or read-only datasources)
3877
+ * simply no-op, and a sync failure must not abort the publish.
3878
+ */
3879
+ async ensureObjectStorage(type, name) {
3880
+ if (type !== "object" && type !== "objects") return;
3881
+ try {
3882
+ await this.engine.syncObjectSchema(name);
3883
+ } catch (err) {
3884
+ console.warn(`[Protocol] table sync failed for object '${name}': ${err?.message ?? err}`);
3885
+ }
3886
+ }
3887
+ /**
3888
+ * Inverse of {@link ensureObjectStorage}: drop an object's physical table.
3889
+ * DESTRUCTIVE — deletes the table and all its rows. Only invoked when a
3890
+ * delete explicitly opts into storage teardown (see {@link deleteMetaItem}'s
3891
+ * `dropStorage`), so publishing an object solely to preview it can be undone
3892
+ * without leaving an orphan table. Best-effort: a failure is logged, not
3893
+ * thrown — the metadata delete already succeeded, and a stray table is
3894
+ * reclaimed by the next sync/drop rather than blocking the delete.
3895
+ */
3896
+ async dropObjectStorage(type, name) {
3897
+ if (type !== "object" && type !== "objects") return;
3898
+ try {
3899
+ await this.engine.dropObjectSchema(name);
3900
+ } catch (err) {
3901
+ console.warn(`[Protocol] table drop failed for object '${name}': ${err?.message ?? err}`);
3902
+ }
3903
+ }
3904
+ /**
3905
+ * Guard for storage teardown on delete. Drops a physical table only when
3906
+ * the caller opted in AND it is safe: object types only (others have no
3907
+ * table), active state only (drafts were never materialised), and never a
3908
+ * `sys_`-prefixed platform table.
3909
+ */
3910
+ shouldDropStorage(type, name, dropStorage, state) {
3911
+ if (!dropStorage) return false;
3912
+ const singular = PLURAL_TO_SINGULAR3[type] ?? type;
3913
+ if (singular !== "object") return false;
3914
+ if (state !== "active") return false;
3915
+ if (name.startsWith("sys_")) return false;
3916
+ return true;
3917
+ }
3772
3918
  async saveMetaItem(request) {
3773
3919
  if (!request.item) {
3774
3920
  throw new Error("Item data is required");
@@ -3842,6 +3988,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3842
3988
  throw err;
3843
3989
  }
3844
3990
  }
3991
+ request.item = normalizeViewMetadata(request.type, request.item, request.name);
3845
3992
  {
3846
3993
  const schema = resolveOverlaySchema(request.type, request.item);
3847
3994
  if (schema) {
@@ -3896,6 +4043,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3896
4043
  });
3897
4044
  if (mode === "publish") {
3898
4045
  this.applyObjectRegistryMutation(request);
4046
+ await this.ensureObjectStorage(request.type, request.name);
3899
4047
  }
3900
4048
  await this.recordMetadataAudit({
3901
4049
  type: request.type,
@@ -4066,6 +4214,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
4066
4214
  name: request.name,
4067
4215
  item: result.item.body
4068
4216
  });
4217
+ await this.ensureObjectStorage(request.type, request.name);
4069
4218
  return {
4070
4219
  success: true,
4071
4220
  version: result.version,
@@ -4086,6 +4235,160 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
4086
4235
  throw err;
4087
4236
  }
4088
4237
  }
4238
+ /**
4239
+ * List pending DRAFT metadata (ADR-0033) for the org, optionally narrowed
4240
+ * by `packageId` and/or `type`. The list reads of `getMetaItems` only see
4241
+ * the ACTIVE registry; this exposes what an AI authored but a human hasn't
4242
+ * published yet, so the console can show a "pending changes" surface and a
4243
+ * just-built app package isn't displayed as empty. No body is returned.
4244
+ */
4245
+ async listDrafts(request) {
4246
+ await this.ensureOverlayIndex();
4247
+ const orgId = request?.organizationId ?? null;
4248
+ const repo = this.getOverlayRepo(orgId);
4249
+ const drafts = await repo.listDrafts({
4250
+ ...request?.type ? { type: PLURAL_TO_SINGULAR3[request.type] ?? request.type } : {},
4251
+ ...request?.packageId ? { packageId: request.packageId } : {}
4252
+ });
4253
+ return { drafts };
4254
+ }
4255
+ /**
4256
+ * Publish every pending DRAFT bound to a package in one shot (ADR-0033) —
4257
+ * the "publish whole app" action. Promotes each draft→active by reusing the
4258
+ * per-item {@link publishMetaItem} primitive (which runs the overridable /
4259
+ * lock guards and refreshes the runtime registry), so this needs NO
4260
+ * `metadata` service (unlike `MetadataService.publishPackage`, which reads
4261
+ * the in-memory registry and 503s when that service is absent). Per-item
4262
+ * failures are collected and do NOT abort the rest.
4263
+ */
4264
+ async publishPackageDrafts(request) {
4265
+ await this.ensureOverlayIndex();
4266
+ const orgId = request.organizationId ?? null;
4267
+ const repo = this.getOverlayRepo(orgId);
4268
+ const drafts = await repo.listDrafts({ packageId: request.packageId });
4269
+ const published = [];
4270
+ const failed = [];
4271
+ for (const d of drafts) {
4272
+ try {
4273
+ const r = await this.publishMetaItem({
4274
+ type: d.type,
4275
+ name: d.name,
4276
+ ...request.organizationId ? { organizationId: request.organizationId } : {},
4277
+ ...request.actor ? { actor: request.actor } : {},
4278
+ message: `publish app package '${request.packageId}'`
4279
+ });
4280
+ published.push({ type: d.type, name: d.name, version: r.version });
4281
+ } catch (e) {
4282
+ failed.push({
4283
+ type: d.type,
4284
+ name: d.name,
4285
+ error: e?.message ?? "publish failed",
4286
+ ...e?.code ? { code: e.code } : {}
4287
+ });
4288
+ }
4289
+ }
4290
+ return {
4291
+ success: failed.length === 0 && published.length > 0,
4292
+ publishedCount: published.length,
4293
+ failedCount: failed.length,
4294
+ published,
4295
+ failed
4296
+ };
4297
+ }
4298
+ /**
4299
+ * Discard every pending DRAFT bound to a package — the NON-destructive
4300
+ * inverse of {@link publishPackageDrafts}. Drops only `state='draft'` rows
4301
+ * (via the per-item delete primitive), reverting the package to its last
4302
+ * published baseline; active/published metadata and physical tables are
4303
+ * left untouched.
4304
+ *
4305
+ * Use case: "I edited this app for a while and it turned out worse than
4306
+ * before — abandon all my changes." Routes through the sys_metadata path
4307
+ * (no metadata-service dependency, unlike `POST /packages/:id/revert`).
4308
+ */
4309
+ async discardPackageDrafts(request) {
4310
+ await this.ensureOverlayIndex();
4311
+ const orgId = request.organizationId ?? null;
4312
+ const repo = this.getOverlayRepo(orgId);
4313
+ const drafts = await repo.listDrafts({ packageId: request.packageId });
4314
+ const discarded = [];
4315
+ const failed = [];
4316
+ for (const d of drafts) {
4317
+ try {
4318
+ await this.deleteMetaItem({
4319
+ type: d.type,
4320
+ name: d.name,
4321
+ state: "draft",
4322
+ ...request.organizationId ? { organizationId: request.organizationId } : {},
4323
+ ...request.actor ? { actor: request.actor } : {}
4324
+ });
4325
+ discarded.push({ type: d.type, name: d.name });
4326
+ } catch (e) {
4327
+ failed.push({
4328
+ type: d.type,
4329
+ name: d.name,
4330
+ error: e?.message ?? "discard failed",
4331
+ ...e?.code ? { code: e.code } : {}
4332
+ });
4333
+ }
4334
+ }
4335
+ return {
4336
+ success: failed.length === 0 && discarded.length > 0,
4337
+ discardedCount: discarded.length,
4338
+ failedCount: failed.length,
4339
+ discarded,
4340
+ failed
4341
+ };
4342
+ }
4343
+ /**
4344
+ * Delete an ENTIRE package: every `sys_metadata` row bound to it (active
4345
+ * AND draft) and — by default — the physical table of each object it
4346
+ * defined. DESTRUCTIVE: removes the app and its data. Use case: "I don't
4347
+ * want this package anymore."
4348
+ *
4349
+ * Set `keepData: true` to remove the metadata but preserve object tables.
4350
+ * The `sys_`-table guard in {@link deleteMetaItem} still applies, so
4351
+ * platform storage is never dropped. Drafts are removed before active rows
4352
+ * so each object's table is torn down once. Per-item failures are collected
4353
+ * without aborting the rest.
4354
+ */
4355
+ async deletePackage(request) {
4356
+ const where = { package_id: request.packageId };
4357
+ if (request.organizationId) where.organization_id = request.organizationId;
4358
+ const rows = await this.engine.find("sys_metadata", { where });
4359
+ const dropStorage = request.keepData !== true;
4360
+ const ordered = [...rows].sort((a, b) => (a.state === "draft" ? 0 : 1) - (b.state === "draft" ? 0 : 1));
4361
+ const deleted = [];
4362
+ const failed = [];
4363
+ for (const row of ordered) {
4364
+ const state = row.state === "draft" ? "draft" : "active";
4365
+ try {
4366
+ await this.deleteMetaItem({
4367
+ type: row.type,
4368
+ name: row.name,
4369
+ state,
4370
+ ...row.organization_id ? { organizationId: row.organization_id } : {},
4371
+ ...request.actor ? { actor: request.actor } : {},
4372
+ ...dropStorage ? { dropStorage: true } : {}
4373
+ });
4374
+ deleted.push({ type: row.type, name: row.name, state });
4375
+ } catch (e) {
4376
+ failed.push({
4377
+ type: row.type,
4378
+ name: row.name,
4379
+ error: e?.message ?? "delete failed",
4380
+ ...e?.code ? { code: e.code } : {}
4381
+ });
4382
+ }
4383
+ }
4384
+ return {
4385
+ success: failed.length === 0 && deleted.length > 0,
4386
+ deletedCount: deleted.length,
4387
+ failedCount: failed.length,
4388
+ deleted,
4389
+ failed
4390
+ };
4391
+ }
4089
4392
  /**
4090
4393
  * Restore the body recorded at history `toVersion` as the new
4091
4394
  * live row. Writes a history event with `op='revert'`. 404
@@ -4318,6 +4621,9 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
4318
4621
  } catch {
4319
4622
  }
4320
4623
  }
4624
+ if (this.shouldDropStorage(request.type, request.name, request.dropStorage, targetState)) {
4625
+ await this.dropObjectStorage(singularTypeForRepo, request.name);
4626
+ }
4321
4627
  await this.recordMetadataAudit({
4322
4628
  type: request.type,
4323
4629
  name: request.name,
@@ -4366,6 +4672,12 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
4366
4672
  };
4367
4673
  }
4368
4674
  await this.engine.delete("sys_metadata", { where: { id: existing.id } });
4675
+ {
4676
+ const targetState = request.state === "draft" ? "draft" : "active";
4677
+ if (this.shouldDropStorage(request.type, request.name, request.dropStorage, targetState)) {
4678
+ await this.dropObjectStorage(PLURAL_TO_SINGULAR3[request.type] ?? request.type, request.name);
4679
+ }
4680
+ }
4369
4681
  if (this.environmentId === void 0) {
4370
4682
  try {
4371
4683
  const services = this.getServicesRegistry?.();
@@ -4623,6 +4935,39 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
4623
4935
  const unsubscribed = await svc.unsubscribe(request.object, request.recordId, "current_user");
4624
4936
  return { success: true, data: { object: request.object, recordId: request.recordId, unsubscribed } };
4625
4937
  }
4938
+ /**
4939
+ * Install a package from a manifest — the single canonical write primitive
4940
+ * for the package subsystem (ADR-0033 consolidation).
4941
+ *
4942
+ * It writes BOTH stores that the runtime keeps for packages, so a package
4943
+ * surfaces consistently no matter which read path is used:
4944
+ * 1. the in-memory `SchemaRegistry` (what the dispatcher's
4945
+ * `/api/v1/packages` list/detail and `getMetaItems({type:'package'})`
4946
+ * read — i.e. what Studio's package selector shows), and
4947
+ * 2. the durable `sys_packages` table via the optional `package` service
4948
+ * (so the package survives a restart; that service re-hydrates these
4949
+ * rows back into the registry on boot).
4950
+ *
4951
+ * The DB write is best-effort and non-fatal: when the `package` service is
4952
+ * absent (e.g. the `marketplace` capability is off) the package is still
4953
+ * registered in-memory and visible for the lifetime of the process.
4954
+ */
4955
+ async installPackage(request) {
4956
+ const manifest = request.manifest;
4957
+ const pkg = this.engine.registry.installPackage(manifest, request.settings);
4958
+ try {
4959
+ const services = this.getServicesRegistry?.();
4960
+ const pkgSvc = services?.get("package");
4961
+ if (pkgSvc?.publish && manifest?.version) {
4962
+ await pkgSvc.publish({ manifest, metadata: {} });
4963
+ }
4964
+ } catch (e) {
4965
+ console.warn(
4966
+ `[protocol.installPackage] sys_packages persist skipped for '${manifest?.id}': ${e?.message}`
4967
+ );
4968
+ }
4969
+ return { package: pkg, message: `Installed package: ${manifest?.id}` };
4970
+ }
4626
4971
  };
4627
4972
  /**
4628
4973
  * Metadata types that are customer-overridable via {@link saveMetaItem}/
@@ -7417,6 +7762,39 @@ var _ObjectQL = class _ObjectQL {
7417
7762
  }
7418
7763
  }
7419
7764
  }
7765
+ /**
7766
+ * Sync a SINGLE object's physical storage (create/alter its table) on
7767
+ * demand. Boot-time {@link syncSchemas} runs once at startup, so an object
7768
+ * that becomes live at runtime (e.g. publishing a drafted object) has a
7769
+ * registry entry but no table — data CRUD then fails with "no such table"
7770
+ * until the next restart. Calling this right after the object is registered
7771
+ * makes it immediately usable. Idempotent: the SQL driver only creates the
7772
+ * table when absent (and alters to add new columns).
7773
+ */
7774
+ async syncObjectSchema(objectName) {
7775
+ const obj = this._registry.getObject(objectName);
7776
+ if (!obj) return;
7777
+ const driver = this.getDriverForObject(objectName);
7778
+ if (!driver || typeof driver.syncSchema !== "function") return;
7779
+ const tableName = StorageNameMapping.resolveTableName(obj);
7780
+ await driver.syncSchema(tableName, obj);
7781
+ }
7782
+ /**
7783
+ * Drop the physical storage (table/collection) backing an object — the
7784
+ * inverse of {@link syncObjectSchema}. DESTRUCTIVE: deletes all rows in the
7785
+ * table. Used by the protocol delete path when the caller explicitly opts
7786
+ * into storage teardown (e.g. discarding an object that was published only
7787
+ * to preview it). No-op when the object's driver does not expose `dropTable`.
7788
+ * Resolves the physical table name from the registered definition, falling
7789
+ * back to the bare name if the def was already removed.
7790
+ */
7791
+ async dropObjectSchema(objectName) {
7792
+ const obj = this._registry.getObject(objectName);
7793
+ const driver = this.getDriverForObject(objectName);
7794
+ if (!driver || typeof driver.dropTable !== "function") return;
7795
+ const tableName = StorageNameMapping.resolveTableName(obj ?? { name: objectName });
7796
+ await driver.dropTable(tableName);
7797
+ }
7420
7798
  /**
7421
7799
  * Get a registered driver by datasource name.
7422
7800
  * Alias matching @objectql/core datasource() API.