@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.js CHANGED
@@ -502,7 +502,7 @@ var LRUCache = class {
502
502
  // src/migrations/add-sys-metadata-overlay-index.ts
503
503
  var INDEX_NAME = "idx_sys_metadata_overlay_active";
504
504
  var TABLE = "sys_metadata";
505
- var COLUMNS = "(type, name, organization_id, project_id, scope)";
505
+ var COLUMNS = "(type, name, organization_id, environment_id, scope)";
506
506
  var WHERE = "state = 'active'";
507
507
  async function addSysMetadataOverlayIndex(driver) {
508
508
  const driverAny = driver;
@@ -541,6 +541,59 @@ async function addSysMetadataOverlayIndex(driver) {
541
541
  }
542
542
  }
543
543
 
544
+ // src/migrations/migrate-project-id-to-environment-id.ts
545
+ var AFFECTED_TABLES = [
546
+ "sys_metadata",
547
+ "sys_metadata_history"
548
+ ];
549
+ async function migrateProjectIdToEnvironmentId(driver) {
550
+ const driverAny = driver;
551
+ if (typeof driverAny.raw !== "function") {
552
+ throw new Error(
553
+ "migrateProjectIdToEnvironmentId: driver must expose a .raw(sql, bindings?) method. SqlDriver (better-sqlite3/knex) and TursoDriver both support this."
554
+ );
555
+ }
556
+ const results = [];
557
+ for (const table of AFFECTED_TABLES) {
558
+ try {
559
+ const hasColumn = await _columnExists(driverAny, table, "project_id");
560
+ const alreadyMigrated = await _columnExists(driverAny, table, "environment_id");
561
+ if (alreadyMigrated && !hasColumn) {
562
+ results.push({ table, status: "already_done" });
563
+ continue;
564
+ }
565
+ if (!hasColumn) {
566
+ results.push({ table, status: "table_missing" });
567
+ continue;
568
+ }
569
+ await driverAny.raw(
570
+ `ALTER TABLE "${table}" RENAME COLUMN project_id TO environment_id`
571
+ );
572
+ results.push({ table, status: "renamed" });
573
+ } catch (err) {
574
+ results.push({ table, status: "error", error: err?.message ?? String(err) });
575
+ }
576
+ }
577
+ return results;
578
+ }
579
+ async function _columnExists(driver, table, column) {
580
+ try {
581
+ const rows = await driver.raw(`PRAGMA table_info("${table}")`);
582
+ if (Array.isArray(rows) && rows.length > 0) {
583
+ const list2 = Array.isArray(rows[0]) ? rows[0] : rows;
584
+ return list2.some((r) => r?.name === column);
585
+ }
586
+ const result = await driver.raw(
587
+ `SELECT column_name FROM information_schema.columns WHERE table_name = ? AND column_name = ?`,
588
+ [table, column]
589
+ );
590
+ const list = Array.isArray(result[0]) ? result[0] : result;
591
+ return list.length > 0;
592
+ } catch {
593
+ return false;
594
+ }
595
+ }
596
+
544
597
  // src/loaders/database-loader.ts
545
598
  var DatabaseLoader = class {
546
599
  constructor(options) {
@@ -564,7 +617,7 @@ var DatabaseLoader = class {
564
617
  this.tableName = options.tableName ?? "sys_metadata";
565
618
  this.historyTableName = options.historyTableName ?? "sys_metadata_history";
566
619
  this.organizationId = options.organizationId;
567
- void options.projectId;
620
+ void options.environmentId;
568
621
  this.trackHistory = options.trackHistory !== false;
569
622
  const cacheOpts = options.cache;
570
623
  const cacheEnabled = cacheOpts?.enabled !== false;
@@ -696,6 +749,7 @@ var DatabaseLoader = class {
696
749
  }
697
750
  }
698
751
  if (driver) {
752
+ await migrateProjectIdToEnvironmentId(driver).catch(() => void 0);
699
753
  await addSysMetadataOverlayIndex(driver);
700
754
  }
701
755
  } catch {
@@ -708,6 +762,10 @@ var DatabaseLoader = class {
708
762
  name: this.tableName
709
763
  });
710
764
  this.schemaReady = true;
765
+ try {
766
+ await migrateProjectIdToEnvironmentId(this.driver);
767
+ } catch {
768
+ }
711
769
  try {
712
770
  await addSysMetadataOverlayIndex(this.driver);
713
771
  } catch {
@@ -738,7 +796,7 @@ var DatabaseLoader = class {
738
796
  }
739
797
  /**
740
798
  * Build base filter conditions for queries.
741
- * Filters by organizationId when configured. `projectId` is accepted
799
+ * Filters by organizationId when configured. `environmentId` is accepted
742
800
  * for back-compat but no longer constrains the query — see
743
801
  * ADR-0008 §0 (branch/project removal).
744
802
  */
@@ -837,7 +895,7 @@ var DatabaseLoader = class {
837
895
  owner: row.owner,
838
896
  state: row.state ?? "active",
839
897
  organizationId: row.organization_id,
840
- projectId: row.project_id,
898
+ environmentId: row.environment_id,
841
899
  version: row.version ?? 1,
842
900
  checksum: row.checksum,
843
901
  source: row.source,
@@ -1271,13 +1329,13 @@ var _MetadataManager = class _MetadataManager {
1271
1329
  *
1272
1330
  * @param driver - An IDataDriver instance for database operations
1273
1331
  * @param organizationId - Organization ID for multi-tenant isolation
1274
- * @param projectId - Project ID (undefined = platform-global)
1332
+ * @param environmentId - Project ID (undefined = platform-global)
1275
1333
  */
1276
- setDatabaseDriver(driver, organizationId, projectId) {
1277
- if (projectId !== void 0) {
1334
+ setDatabaseDriver(driver, organizationId, environmentId) {
1335
+ if (environmentId !== void 0) {
1278
1336
  this.logger.info("Project kernel \u2014 skipping DatabaseLoader for sys_metadata (control-plane only)", {
1279
1337
  organizationId,
1280
- projectId
1338
+ environmentId
1281
1339
  });
1282
1340
  return;
1283
1341
  }
@@ -1286,7 +1344,7 @@ var _MetadataManager = class _MetadataManager {
1286
1344
  driver,
1287
1345
  tableName,
1288
1346
  organizationId,
1289
- projectId,
1347
+ environmentId,
1290
1348
  cache: this.config.cache?.databaseLoader
1291
1349
  });
1292
1350
  this.registerLoader(dbLoader);
@@ -1300,13 +1358,13 @@ var _MetadataManager = class _MetadataManager {
1300
1358
  *
1301
1359
  * @param engine - An IDataEngine instance (typically the ObjectQL service)
1302
1360
  * @param organizationId - Organization ID for multi-tenant isolation
1303
- * @param projectId - Project ID (undefined = platform-global)
1361
+ * @param environmentId - Project ID (undefined = platform-global)
1304
1362
  */
1305
- setDataEngine(engine, organizationId, projectId) {
1306
- if (projectId !== void 0) {
1363
+ setDataEngine(engine, organizationId, environmentId) {
1364
+ if (environmentId !== void 0) {
1307
1365
  this.logger.info("Project kernel \u2014 skipping DatabaseLoader for sys_metadata (control-plane only)", {
1308
1366
  organizationId,
1309
- projectId
1367
+ environmentId
1310
1368
  });
1311
1369
  return;
1312
1370
  }
@@ -1315,7 +1373,7 @@ var _MetadataManager = class _MetadataManager {
1315
1373
  engine,
1316
1374
  tableName,
1317
1375
  organizationId,
1318
- projectId,
1376
+ environmentId,
1319
1377
  cache: this.config.cache?.databaseLoader
1320
1378
  });
1321
1379
  this.registerLoader(dbLoader);
@@ -2348,6 +2406,23 @@ var _MetadataManager = class _MetadataManager {
2348
2406
  this.notifyWatchers(type, legacyEvent);
2349
2407
  }
2350
2408
  notifyWatchers(type, event) {
2409
+ this.notifyWatchersLocal(type, event);
2410
+ if (this.clusterPubSub) {
2411
+ const payload = {
2412
+ originNode: this.clusterNodeId,
2413
+ type,
2414
+ event
2415
+ };
2416
+ const key = `${type}:${event.name ?? ""}`;
2417
+ void this.clusterPubSub.publish(_MetadataManager.CLUSTER_CHANNEL, payload, { partitionKey: key }).catch((err) => {
2418
+ this.logger.error("Cluster metadata publish failed", void 0, {
2419
+ type,
2420
+ error: err instanceof Error ? err.message : String(err)
2421
+ });
2422
+ });
2423
+ }
2424
+ }
2425
+ notifyWatchersLocal(type, event) {
2351
2426
  const callbacks = this.watchCallbacks.get(type);
2352
2427
  if (!callbacks) return;
2353
2428
  for (const callback of callbacks) {
@@ -2361,6 +2436,63 @@ var _MetadataManager = class _MetadataManager {
2361
2436
  }
2362
2437
  }
2363
2438
  }
2439
+ /**
2440
+ * Attach a cluster pub/sub transport so metadata-change events fan
2441
+ * out to peer nodes and remote events replay into local watchers.
2442
+ *
2443
+ * The bridge plugin in @objectstack/service-cluster calls this once
2444
+ * per kernel boot after both cluster and metadata services are
2445
+ * registered. Passing the same MetadataManager twice no-ops; passing
2446
+ * a different transport replaces the prior subscription.
2447
+ *
2448
+ * Pass `nodeId` matching the local cluster's nodeId so loopback
2449
+ * suppression works.
2450
+ *
2451
+ * @returns disposer that unsubscribes from cluster events.
2452
+ */
2453
+ attachClusterPubSub(pubsub, nodeId) {
2454
+ if (this.clusterPubSub === pubsub && this.clusterNodeId === nodeId) {
2455
+ return () => this.detachClusterPubSub();
2456
+ }
2457
+ this.detachClusterPubSub();
2458
+ this.clusterPubSub = pubsub;
2459
+ this.clusterNodeId = nodeId;
2460
+ this.clusterUnsubscribe = pubsub.subscribe(
2461
+ _MetadataManager.CLUSTER_CHANNEL,
2462
+ (msg) => {
2463
+ const p = msg.payload;
2464
+ if (p?.originNode && p.originNode === this.clusterNodeId) return;
2465
+ if (!p?.type || !p.event) return;
2466
+ setImmediate(() => {
2467
+ try {
2468
+ this.notifyWatchersLocal(p.type, p.event);
2469
+ } catch (err) {
2470
+ this.logger.error("Cluster remote replay failed", void 0, {
2471
+ type: p.type,
2472
+ error: err instanceof Error ? err.message : String(err)
2473
+ });
2474
+ }
2475
+ });
2476
+ }
2477
+ );
2478
+ this.logger.info("MetadataManager attached to cluster pubsub", {
2479
+ nodeId,
2480
+ channel: _MetadataManager.CLUSTER_CHANNEL
2481
+ });
2482
+ return () => this.detachClusterPubSub();
2483
+ }
2484
+ /** Tear down cluster wiring. Safe to call multiple times. */
2485
+ detachClusterPubSub() {
2486
+ if (this.clusterUnsubscribe) {
2487
+ try {
2488
+ this.clusterUnsubscribe();
2489
+ } catch {
2490
+ }
2491
+ this.clusterUnsubscribe = void 0;
2492
+ }
2493
+ this.clusterPubSub = void 0;
2494
+ this.clusterNodeId = void 0;
2495
+ }
2364
2496
  // ==========================================
2365
2497
  // Version History & Rollback
2366
2498
  // ==========================================
@@ -2461,6 +2593,7 @@ var _MetadataManager = class _MetadataManager {
2461
2593
  }
2462
2594
  };
2463
2595
  _MetadataManager.LIST_CACHE_TTL_MS = 3e4;
2596
+ _MetadataManager.CLUSTER_CHANNEL = "metadata.changed";
2464
2597
  var MetadataManager = _MetadataManager;
2465
2598
 
2466
2599
  // src/plugin.ts
@@ -3242,24 +3375,24 @@ var MetadataPlugin = class {
3242
3375
  * metadata items into the MetadataManager.
3243
3376
  */
3244
3377
  async _parseAndRegisterArtifact(ctx, raw, label) {
3245
- const { ProjectArtifactSchema } = await import("@objectstack/spec/cloud");
3378
+ const { EnvironmentArtifactSchema } = await import("@objectstack/spec/cloud");
3246
3379
  const { ObjectStackDefinitionSchema } = await import("@objectstack/spec");
3247
3380
  let metadata;
3248
3381
  const obj = raw;
3249
3382
  if (obj?.schemaVersion && obj?.commitId && obj?.metadata !== void 0) {
3250
- const artifact = ProjectArtifactSchema.parse(obj);
3383
+ const artifact = EnvironmentArtifactSchema.parse(obj);
3251
3384
  metadata = artifact.metadata;
3252
3385
  } else if (obj?.success && obj?.data?.metadata) {
3253
- const artifact = ProjectArtifactSchema.parse(obj.data);
3386
+ const artifact = EnvironmentArtifactSchema.parse(obj.data);
3254
3387
  metadata = artifact.metadata;
3255
3388
  } else {
3256
3389
  const def = ObjectStackDefinitionSchema.parse(obj);
3257
3390
  const canonical = JSON.stringify(def, Object.keys(def).sort());
3258
3391
  const checksum = createHash2("sha256").update(canonical).digest("hex");
3259
- const projectId = this.options.projectId ?? "proj_local";
3260
- ProjectArtifactSchema.parse({
3392
+ const environmentId = this.options.environmentId ?? "proj_local";
3393
+ EnvironmentArtifactSchema.parse({
3261
3394
  schemaVersion: "0.1",
3262
- projectId,
3395
+ environmentId,
3263
3396
  commitId: "local-dev",
3264
3397
  checksum,
3265
3398
  metadata: def
@@ -3315,13 +3448,13 @@ var MetadataPlugin = class {
3315
3448
  * P2: Load metadata from the cloud artifact API endpoint.
3316
3449
  */
3317
3450
  async _loadFromArtifactApi(ctx, src) {
3318
- const projectId = this.options.projectId;
3319
- if (!projectId) {
3320
- throw new Error("[MetadataPlugin] artifact-api source requires options.projectId to be set");
3451
+ const environmentId = this.options.environmentId;
3452
+ if (!environmentId) {
3453
+ throw new Error("[MetadataPlugin] artifact-api source requires options.environmentId to be set");
3321
3454
  }
3322
3455
  let artifactUrl = src.url.replace(/\/+$/, "");
3323
3456
  if (!/\/api\/v\d+\/cloud\/projects\//i.test(artifactUrl)) {
3324
- artifactUrl = `${artifactUrl}/api/v1/cloud/projects/${projectId}/artifact`;
3457
+ artifactUrl = `${artifactUrl}/api/v1/cloud/environments/${environmentId}/artifact`;
3325
3458
  }
3326
3459
  if (src.commitId) {
3327
3460
  artifactUrl += `${artifactUrl.includes("?") ? "&" : "?"}commit=${encodeURIComponent(src.commitId)}`;