@objectstack/metadata 5.1.0 → 6.0.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/node.cjs CHANGED
@@ -548,7 +548,7 @@ var LRUCache = class {
548
548
  // src/migrations/add-sys-metadata-overlay-index.ts
549
549
  var INDEX_NAME = "idx_sys_metadata_overlay_active";
550
550
  var TABLE = "sys_metadata";
551
- var COLUMNS = "(type, name, organization_id, project_id, scope)";
551
+ var COLUMNS = "(type, name, organization_id, environment_id, scope)";
552
552
  var WHERE = "state = 'active'";
553
553
  async function addSysMetadataOverlayIndex(driver) {
554
554
  const driverAny = driver;
@@ -587,6 +587,59 @@ async function addSysMetadataOverlayIndex(driver) {
587
587
  }
588
588
  }
589
589
 
590
+ // src/migrations/migrate-project-id-to-environment-id.ts
591
+ var AFFECTED_TABLES = [
592
+ "sys_metadata",
593
+ "sys_metadata_history"
594
+ ];
595
+ async function migrateProjectIdToEnvironmentId(driver) {
596
+ const driverAny = driver;
597
+ if (typeof driverAny.raw !== "function") {
598
+ throw new Error(
599
+ "migrateProjectIdToEnvironmentId: driver must expose a .raw(sql, bindings?) method. SqlDriver (better-sqlite3/knex) and TursoDriver both support this."
600
+ );
601
+ }
602
+ const results = [];
603
+ for (const table of AFFECTED_TABLES) {
604
+ try {
605
+ const hasColumn = await _columnExists(driverAny, table, "project_id");
606
+ const alreadyMigrated = await _columnExists(driverAny, table, "environment_id");
607
+ if (alreadyMigrated && !hasColumn) {
608
+ results.push({ table, status: "already_done" });
609
+ continue;
610
+ }
611
+ if (!hasColumn) {
612
+ results.push({ table, status: "table_missing" });
613
+ continue;
614
+ }
615
+ await driverAny.raw(
616
+ `ALTER TABLE "${table}" RENAME COLUMN project_id TO environment_id`
617
+ );
618
+ results.push({ table, status: "renamed" });
619
+ } catch (err) {
620
+ results.push({ table, status: "error", error: err?.message ?? String(err) });
621
+ }
622
+ }
623
+ return results;
624
+ }
625
+ async function _columnExists(driver, table, column) {
626
+ try {
627
+ const rows = await driver.raw(`PRAGMA table_info("${table}")`);
628
+ if (Array.isArray(rows) && rows.length > 0) {
629
+ const list2 = Array.isArray(rows[0]) ? rows[0] : rows;
630
+ return list2.some((r) => r?.name === column);
631
+ }
632
+ const result = await driver.raw(
633
+ `SELECT column_name FROM information_schema.columns WHERE table_name = ? AND column_name = ?`,
634
+ [table, column]
635
+ );
636
+ const list = Array.isArray(result[0]) ? result[0] : result;
637
+ return list.length > 0;
638
+ } catch {
639
+ return false;
640
+ }
641
+ }
642
+
590
643
  // src/loaders/database-loader.ts
591
644
  var DatabaseLoader = class {
592
645
  constructor(options) {
@@ -610,7 +663,7 @@ var DatabaseLoader = class {
610
663
  this.tableName = options.tableName ?? "sys_metadata";
611
664
  this.historyTableName = options.historyTableName ?? "sys_metadata_history";
612
665
  this.organizationId = options.organizationId;
613
- void options.projectId;
666
+ void options.environmentId;
614
667
  this.trackHistory = options.trackHistory !== false;
615
668
  const cacheOpts = options.cache;
616
669
  const cacheEnabled = cacheOpts?.enabled !== false;
@@ -742,6 +795,7 @@ var DatabaseLoader = class {
742
795
  }
743
796
  }
744
797
  if (driver) {
798
+ await migrateProjectIdToEnvironmentId(driver).catch(() => void 0);
745
799
  await addSysMetadataOverlayIndex(driver);
746
800
  }
747
801
  } catch {
@@ -754,6 +808,10 @@ var DatabaseLoader = class {
754
808
  name: this.tableName
755
809
  });
756
810
  this.schemaReady = true;
811
+ try {
812
+ await migrateProjectIdToEnvironmentId(this.driver);
813
+ } catch {
814
+ }
757
815
  try {
758
816
  await addSysMetadataOverlayIndex(this.driver);
759
817
  } catch {
@@ -784,7 +842,7 @@ var DatabaseLoader = class {
784
842
  }
785
843
  /**
786
844
  * Build base filter conditions for queries.
787
- * Filters by organizationId when configured. `projectId` is accepted
845
+ * Filters by organizationId when configured. `environmentId` is accepted
788
846
  * for back-compat but no longer constrains the query — see
789
847
  * ADR-0008 §0 (branch/project removal).
790
848
  */
@@ -883,7 +941,7 @@ var DatabaseLoader = class {
883
941
  owner: row.owner,
884
942
  state: row.state ?? "active",
885
943
  organizationId: row.organization_id,
886
- projectId: row.project_id,
944
+ environmentId: row.environment_id,
887
945
  version: row.version ?? 1,
888
946
  checksum: row.checksum,
889
947
  source: row.source,
@@ -1317,13 +1375,13 @@ var _MetadataManager = class _MetadataManager {
1317
1375
  *
1318
1376
  * @param driver - An IDataDriver instance for database operations
1319
1377
  * @param organizationId - Organization ID for multi-tenant isolation
1320
- * @param projectId - Project ID (undefined = platform-global)
1378
+ * @param environmentId - Project ID (undefined = platform-global)
1321
1379
  */
1322
- setDatabaseDriver(driver, organizationId, projectId) {
1323
- if (projectId !== void 0) {
1380
+ setDatabaseDriver(driver, organizationId, environmentId) {
1381
+ if (environmentId !== void 0) {
1324
1382
  this.logger.info("Project kernel \u2014 skipping DatabaseLoader for sys_metadata (control-plane only)", {
1325
1383
  organizationId,
1326
- projectId
1384
+ environmentId
1327
1385
  });
1328
1386
  return;
1329
1387
  }
@@ -1332,7 +1390,7 @@ var _MetadataManager = class _MetadataManager {
1332
1390
  driver,
1333
1391
  tableName,
1334
1392
  organizationId,
1335
- projectId,
1393
+ environmentId,
1336
1394
  cache: this.config.cache?.databaseLoader
1337
1395
  });
1338
1396
  this.registerLoader(dbLoader);
@@ -1346,13 +1404,13 @@ var _MetadataManager = class _MetadataManager {
1346
1404
  *
1347
1405
  * @param engine - An IDataEngine instance (typically the ObjectQL service)
1348
1406
  * @param organizationId - Organization ID for multi-tenant isolation
1349
- * @param projectId - Project ID (undefined = platform-global)
1407
+ * @param environmentId - Project ID (undefined = platform-global)
1350
1408
  */
1351
- setDataEngine(engine, organizationId, projectId) {
1352
- if (projectId !== void 0) {
1409
+ setDataEngine(engine, organizationId, environmentId) {
1410
+ if (environmentId !== void 0) {
1353
1411
  this.logger.info("Project kernel \u2014 skipping DatabaseLoader for sys_metadata (control-plane only)", {
1354
1412
  organizationId,
1355
- projectId
1413
+ environmentId
1356
1414
  });
1357
1415
  return;
1358
1416
  }
@@ -1361,7 +1419,7 @@ var _MetadataManager = class _MetadataManager {
1361
1419
  engine,
1362
1420
  tableName,
1363
1421
  organizationId,
1364
- projectId,
1422
+ environmentId,
1365
1423
  cache: this.config.cache?.databaseLoader
1366
1424
  });
1367
1425
  this.registerLoader(dbLoader);
@@ -2394,6 +2452,23 @@ var _MetadataManager = class _MetadataManager {
2394
2452
  this.notifyWatchers(type, legacyEvent);
2395
2453
  }
2396
2454
  notifyWatchers(type, event) {
2455
+ this.notifyWatchersLocal(type, event);
2456
+ if (this.clusterPubSub) {
2457
+ const payload = {
2458
+ originNode: this.clusterNodeId,
2459
+ type,
2460
+ event
2461
+ };
2462
+ const key = `${type}:${event.name ?? ""}`;
2463
+ void this.clusterPubSub.publish(_MetadataManager.CLUSTER_CHANNEL, payload, { partitionKey: key }).catch((err) => {
2464
+ this.logger.error("Cluster metadata publish failed", void 0, {
2465
+ type,
2466
+ error: err instanceof Error ? err.message : String(err)
2467
+ });
2468
+ });
2469
+ }
2470
+ }
2471
+ notifyWatchersLocal(type, event) {
2397
2472
  const callbacks = this.watchCallbacks.get(type);
2398
2473
  if (!callbacks) return;
2399
2474
  for (const callback of callbacks) {
@@ -2407,6 +2482,63 @@ var _MetadataManager = class _MetadataManager {
2407
2482
  }
2408
2483
  }
2409
2484
  }
2485
+ /**
2486
+ * Attach a cluster pub/sub transport so metadata-change events fan
2487
+ * out to peer nodes and remote events replay into local watchers.
2488
+ *
2489
+ * The bridge plugin in @objectstack/service-cluster calls this once
2490
+ * per kernel boot after both cluster and metadata services are
2491
+ * registered. Passing the same MetadataManager twice no-ops; passing
2492
+ * a different transport replaces the prior subscription.
2493
+ *
2494
+ * Pass `nodeId` matching the local cluster's nodeId so loopback
2495
+ * suppression works.
2496
+ *
2497
+ * @returns disposer that unsubscribes from cluster events.
2498
+ */
2499
+ attachClusterPubSub(pubsub, nodeId) {
2500
+ if (this.clusterPubSub === pubsub && this.clusterNodeId === nodeId) {
2501
+ return () => this.detachClusterPubSub();
2502
+ }
2503
+ this.detachClusterPubSub();
2504
+ this.clusterPubSub = pubsub;
2505
+ this.clusterNodeId = nodeId;
2506
+ this.clusterUnsubscribe = pubsub.subscribe(
2507
+ _MetadataManager.CLUSTER_CHANNEL,
2508
+ (msg) => {
2509
+ const p = msg.payload;
2510
+ if (p?.originNode && p.originNode === this.clusterNodeId) return;
2511
+ if (!p?.type || !p.event) return;
2512
+ setImmediate(() => {
2513
+ try {
2514
+ this.notifyWatchersLocal(p.type, p.event);
2515
+ } catch (err) {
2516
+ this.logger.error("Cluster remote replay failed", void 0, {
2517
+ type: p.type,
2518
+ error: err instanceof Error ? err.message : String(err)
2519
+ });
2520
+ }
2521
+ });
2522
+ }
2523
+ );
2524
+ this.logger.info("MetadataManager attached to cluster pubsub", {
2525
+ nodeId,
2526
+ channel: _MetadataManager.CLUSTER_CHANNEL
2527
+ });
2528
+ return () => this.detachClusterPubSub();
2529
+ }
2530
+ /** Tear down cluster wiring. Safe to call multiple times. */
2531
+ detachClusterPubSub() {
2532
+ if (this.clusterUnsubscribe) {
2533
+ try {
2534
+ this.clusterUnsubscribe();
2535
+ } catch {
2536
+ }
2537
+ this.clusterUnsubscribe = void 0;
2538
+ }
2539
+ this.clusterPubSub = void 0;
2540
+ this.clusterNodeId = void 0;
2541
+ }
2410
2542
  // ==========================================
2411
2543
  // Version History & Rollback
2412
2544
  // ==========================================
@@ -2507,6 +2639,7 @@ var _MetadataManager = class _MetadataManager {
2507
2639
  }
2508
2640
  };
2509
2641
  _MetadataManager.LIST_CACHE_TTL_MS = 3e4;
2642
+ _MetadataManager.CLUSTER_CHANNEL = "metadata.changed";
2510
2643
  var MetadataManager = _MetadataManager;
2511
2644
 
2512
2645
  // src/plugin.ts
@@ -3285,24 +3418,24 @@ var MetadataPlugin = class {
3285
3418
  * metadata items into the MetadataManager.
3286
3419
  */
3287
3420
  async _parseAndRegisterArtifact(ctx, raw, label) {
3288
- const { ProjectArtifactSchema } = await import("@objectstack/spec/cloud");
3421
+ const { EnvironmentArtifactSchema } = await import("@objectstack/spec/cloud");
3289
3422
  const { ObjectStackDefinitionSchema } = await import("@objectstack/spec");
3290
3423
  let metadata;
3291
3424
  const obj = raw;
3292
3425
  if (obj?.schemaVersion && obj?.commitId && obj?.metadata !== void 0) {
3293
- const artifact = ProjectArtifactSchema.parse(obj);
3426
+ const artifact = EnvironmentArtifactSchema.parse(obj);
3294
3427
  metadata = artifact.metadata;
3295
3428
  } else if (obj?.success && obj?.data?.metadata) {
3296
- const artifact = ProjectArtifactSchema.parse(obj.data);
3429
+ const artifact = EnvironmentArtifactSchema.parse(obj.data);
3297
3430
  metadata = artifact.metadata;
3298
3431
  } else {
3299
3432
  const def = ObjectStackDefinitionSchema.parse(obj);
3300
3433
  const canonical = JSON.stringify(def, Object.keys(def).sort());
3301
3434
  const checksum = (0, import_node_crypto2.createHash)("sha256").update(canonical).digest("hex");
3302
- const projectId = this.options.projectId ?? "proj_local";
3303
- ProjectArtifactSchema.parse({
3435
+ const environmentId = this.options.environmentId ?? "proj_local";
3436
+ EnvironmentArtifactSchema.parse({
3304
3437
  schemaVersion: "0.1",
3305
- projectId,
3438
+ environmentId,
3306
3439
  commitId: "local-dev",
3307
3440
  checksum,
3308
3441
  metadata: def
@@ -3358,13 +3491,13 @@ var MetadataPlugin = class {
3358
3491
  * P2: Load metadata from the cloud artifact API endpoint.
3359
3492
  */
3360
3493
  async _loadFromArtifactApi(ctx, src) {
3361
- const projectId = this.options.projectId;
3362
- if (!projectId) {
3363
- throw new Error("[MetadataPlugin] artifact-api source requires options.projectId to be set");
3494
+ const environmentId = this.options.environmentId;
3495
+ if (!environmentId) {
3496
+ throw new Error("[MetadataPlugin] artifact-api source requires options.environmentId to be set");
3364
3497
  }
3365
3498
  let artifactUrl = src.url.replace(/\/+$/, "");
3366
3499
  if (!/\/api\/v\d+\/cloud\/projects\//i.test(artifactUrl)) {
3367
- artifactUrl = `${artifactUrl}/api/v1/cloud/projects/${projectId}/artifact`;
3500
+ artifactUrl = `${artifactUrl}/api/v1/cloud/environments/${environmentId}/artifact`;
3368
3501
  }
3369
3502
  if (src.commitId) {
3370
3503
  artifactUrl += `${artifactUrl.includes("?") ? "&" : "?"}commit=${encodeURIComponent(src.commitId)}`;