@objectstack/metadata 5.0.0 → 5.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.cjs CHANGED
@@ -799,7 +799,6 @@ var DatabaseLoader = class {
799
799
  /**
800
800
  * Create a history record for a metadata change.
801
801
  *
802
- * @param metadataId - The metadata record ID
803
802
  * @param type - Metadata type
804
803
  * @param name - Metadata name
805
804
  * @param version - Version number
@@ -809,7 +808,7 @@ var DatabaseLoader = class {
809
808
  * @param changeNote - Optional change description
810
809
  * @param recordedBy - Optional user who made the change
811
810
  */
812
- async createHistoryRecord(metadataId, type, name, version, metadata, operationType, previousChecksum, changeNote, recordedBy) {
811
+ async createHistoryRecord(type, name, version, metadata, operationType, previousChecksum, changeNote, recordedBy) {
813
812
  if (!this.trackHistory) return;
814
813
  await this.ensureHistorySchema();
815
814
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -822,7 +821,6 @@ var DatabaseLoader = class {
822
821
  const eventSeq = await this.nextEventSeq();
823
822
  const historyRecord = {
824
823
  id: historyId,
825
- metadataId,
826
824
  name,
827
825
  type,
828
826
  version,
@@ -839,7 +837,6 @@ var DatabaseLoader = class {
839
837
  await this._create(this.historyTableName, {
840
838
  id: historyRecord.id,
841
839
  event_seq: eventSeq,
842
- metadata_id: historyRecord.metadataId,
843
840
  name: historyRecord.name,
844
841
  type: historyRecord.type,
845
842
  version: historyRecord.version,
@@ -1027,12 +1024,9 @@ var DatabaseLoader = class {
1027
1024
  async getHistoryRecord(type, name, version) {
1028
1025
  if (!this.trackHistory) return null;
1029
1026
  await this.ensureHistorySchema();
1030
- const metadataRow = await this._findOne(this.tableName, {
1031
- where: this.baseFilter(type, name)
1032
- });
1033
- if (!metadataRow) return null;
1034
1027
  const filter = {
1035
- metadata_id: metadataRow.id,
1028
+ type,
1029
+ name,
1036
1030
  version
1037
1031
  };
1038
1032
  if (this.organizationId) {
@@ -1044,7 +1038,6 @@ var DatabaseLoader = class {
1044
1038
  if (!row) return null;
1045
1039
  return {
1046
1040
  id: row.id,
1047
- metadataId: row.metadata_id,
1048
1041
  name: row.name,
1049
1042
  type: row.type,
1050
1043
  version: row.version,
@@ -1069,14 +1062,9 @@ var DatabaseLoader = class {
1069
1062
  }
1070
1063
  await this.ensureSchema();
1071
1064
  await this.ensureHistorySchema();
1072
- const filter = { type, name };
1073
- if (this.organizationId) filter.organization_id = this.organizationId;
1074
- const metadataRecord = await this._findOne(this.tableName, { where: filter });
1075
- if (!metadataRecord) {
1076
- return { records: [], total: 0, hasMore: false };
1077
- }
1078
1065
  const historyFilter = {
1079
- metadata_id: metadataRecord.id
1066
+ type,
1067
+ name
1080
1068
  };
1081
1069
  if (this.organizationId) historyFilter.organization_id = this.organizationId;
1082
1070
  if (options?.operationType) historyFilter.operation_type = options.operationType;
@@ -1107,7 +1095,6 @@ var DatabaseLoader = class {
1107
1095
  const parsedMetadata = typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata;
1108
1096
  return {
1109
1097
  id: row.id,
1110
- metadataId: row.metadata_id,
1111
1098
  name: row.name,
1112
1099
  type: row.type,
1113
1100
  version: row.version,
@@ -1152,7 +1139,6 @@ var DatabaseLoader = class {
1152
1139
  });
1153
1140
  this.invalidate(type, name);
1154
1141
  await this.createHistoryRecord(
1155
- existing.id,
1156
1142
  type,
1157
1143
  name,
1158
1144
  newVersion,
@@ -1194,7 +1180,6 @@ var DatabaseLoader = class {
1194
1180
  });
1195
1181
  this.invalidate(type, name);
1196
1182
  await this.createHistoryRecord(
1197
- existing.id,
1198
1183
  type,
1199
1184
  name,
1200
1185
  version,
@@ -1228,7 +1213,6 @@ var DatabaseLoader = class {
1228
1213
  });
1229
1214
  this.invalidate(type, name);
1230
1215
  await this.createHistoryRecord(
1231
- id,
1232
1216
  type,
1233
1217
  name,
1234
1218
  1,
@@ -2408,6 +2392,23 @@ var _MetadataManager = class _MetadataManager {
2408
2392
  this.notifyWatchers(type, legacyEvent);
2409
2393
  }
2410
2394
  notifyWatchers(type, event) {
2395
+ this.notifyWatchersLocal(type, event);
2396
+ if (this.clusterPubSub) {
2397
+ const payload = {
2398
+ originNode: this.clusterNodeId,
2399
+ type,
2400
+ event
2401
+ };
2402
+ const key = `${type}:${event.name ?? ""}`;
2403
+ void this.clusterPubSub.publish(_MetadataManager.CLUSTER_CHANNEL, payload, { partitionKey: key }).catch((err) => {
2404
+ this.logger.error("Cluster metadata publish failed", void 0, {
2405
+ type,
2406
+ error: err instanceof Error ? err.message : String(err)
2407
+ });
2408
+ });
2409
+ }
2410
+ }
2411
+ notifyWatchersLocal(type, event) {
2411
2412
  const callbacks = this.watchCallbacks.get(type);
2412
2413
  if (!callbacks) return;
2413
2414
  for (const callback of callbacks) {
@@ -2421,6 +2422,63 @@ var _MetadataManager = class _MetadataManager {
2421
2422
  }
2422
2423
  }
2423
2424
  }
2425
+ /**
2426
+ * Attach a cluster pub/sub transport so metadata-change events fan
2427
+ * out to peer nodes and remote events replay into local watchers.
2428
+ *
2429
+ * The bridge plugin in @objectstack/service-cluster calls this once
2430
+ * per kernel boot after both cluster and metadata services are
2431
+ * registered. Passing the same MetadataManager twice no-ops; passing
2432
+ * a different transport replaces the prior subscription.
2433
+ *
2434
+ * Pass `nodeId` matching the local cluster's nodeId so loopback
2435
+ * suppression works.
2436
+ *
2437
+ * @returns disposer that unsubscribes from cluster events.
2438
+ */
2439
+ attachClusterPubSub(pubsub, nodeId) {
2440
+ if (this.clusterPubSub === pubsub && this.clusterNodeId === nodeId) {
2441
+ return () => this.detachClusterPubSub();
2442
+ }
2443
+ this.detachClusterPubSub();
2444
+ this.clusterPubSub = pubsub;
2445
+ this.clusterNodeId = nodeId;
2446
+ this.clusterUnsubscribe = pubsub.subscribe(
2447
+ _MetadataManager.CLUSTER_CHANNEL,
2448
+ (msg) => {
2449
+ const p = msg.payload;
2450
+ if (p?.originNode && p.originNode === this.clusterNodeId) return;
2451
+ if (!p?.type || !p.event) return;
2452
+ setImmediate(() => {
2453
+ try {
2454
+ this.notifyWatchersLocal(p.type, p.event);
2455
+ } catch (err) {
2456
+ this.logger.error("Cluster remote replay failed", void 0, {
2457
+ type: p.type,
2458
+ error: err instanceof Error ? err.message : String(err)
2459
+ });
2460
+ }
2461
+ });
2462
+ }
2463
+ );
2464
+ this.logger.info("MetadataManager attached to cluster pubsub", {
2465
+ nodeId,
2466
+ channel: _MetadataManager.CLUSTER_CHANNEL
2467
+ });
2468
+ return () => this.detachClusterPubSub();
2469
+ }
2470
+ /** Tear down cluster wiring. Safe to call multiple times. */
2471
+ detachClusterPubSub() {
2472
+ if (this.clusterUnsubscribe) {
2473
+ try {
2474
+ this.clusterUnsubscribe();
2475
+ } catch {
2476
+ }
2477
+ this.clusterUnsubscribe = void 0;
2478
+ }
2479
+ this.clusterPubSub = void 0;
2480
+ this.clusterNodeId = void 0;
2481
+ }
2424
2482
  // ==========================================
2425
2483
  // Version History & Rollback
2426
2484
  // ==========================================
@@ -2521,6 +2579,7 @@ var _MetadataManager = class _MetadataManager {
2521
2579
  }
2522
2580
  };
2523
2581
  _MetadataManager.LIST_CACHE_TTL_MS = 3e4;
2582
+ _MetadataManager.CLUSTER_CHANNEL = "metadata.changed";
2524
2583
  var MetadataManager = _MetadataManager;
2525
2584
 
2526
2585
  // src/plugin.ts
@@ -3644,6 +3703,10 @@ function registerMetadataHistoryRoutes(app, metadataService) {
3644
3703
  }
3645
3704
 
3646
3705
  // src/utils/history-cleanup.ts
3706
+ var import_kernel2 = require("@objectstack/spec/kernel");
3707
+ function executionPinnedTypes() {
3708
+ return import_kernel2.DEFAULT_METADATA_TYPE_REGISTRY.filter((entry) => entry.executionPinned).map((entry) => entry.type);
3709
+ }
3647
3710
  var HistoryCleanupManager = class {
3648
3711
  constructor(policy, dbLoader) {
3649
3712
  this.policy = policy;
@@ -3681,6 +3744,8 @@ var HistoryCleanupManager = class {
3681
3744
  const organizationId = this.dbLoader.organizationId;
3682
3745
  let deleted = 0;
3683
3746
  let errors = 0;
3747
+ const pinnedTypes = executionPinnedTypes();
3748
+ const isPinned = (t) => !!t && pinnedTypes.includes(t);
3684
3749
  try {
3685
3750
  if (this.policy.maxAgeDays) {
3686
3751
  const cutoffDate = /* @__PURE__ */ new Date();
@@ -3692,6 +3757,9 @@ var HistoryCleanupManager = class {
3692
3757
  if (organizationId) {
3693
3758
  filter.organization_id = organizationId;
3694
3759
  }
3760
+ if (pinnedTypes.length > 0) {
3761
+ filter.type = { $nin: pinnedTypes };
3762
+ }
3695
3763
  try {
3696
3764
  const result = await this.bulkDeleteByFilter(driver, historyTableName, filter);
3697
3765
  deleted += result.deleted;
@@ -3704,19 +3772,22 @@ var HistoryCleanupManager = class {
3704
3772
  try {
3705
3773
  const baseWhere = {};
3706
3774
  if (organizationId) baseWhere.organization_id = organizationId;
3707
- const metadataIds = await driver.find(historyTableName, {
3775
+ const metaItems = await driver.find(historyTableName, {
3708
3776
  object: historyTableName,
3709
3777
  where: baseWhere,
3710
- fields: ["metadata_id"]
3778
+ fields: ["type", "name"]
3711
3779
  });
3712
- const uniqueIds = /* @__PURE__ */ new Set();
3713
- for (const record of metadataIds) {
3714
- if (record.metadata_id) {
3715
- uniqueIds.add(record.metadata_id);
3780
+ const uniqueKeys = /* @__PURE__ */ new Set();
3781
+ for (const record of metaItems) {
3782
+ const t = record.type;
3783
+ const n = record.name;
3784
+ if (t && n && !isPinned(t)) {
3785
+ uniqueKeys.add(`${t}${n}`);
3716
3786
  }
3717
3787
  }
3718
- for (const metadataId of uniqueIds) {
3719
- const filter = { metadata_id: metadataId, ...baseWhere };
3788
+ for (const key of uniqueKeys) {
3789
+ const [type, name] = key.split("");
3790
+ const filter = { type, name, ...baseWhere };
3720
3791
  try {
3721
3792
  const historyRecords = await driver.find(historyTableName, {
3722
3793
  object: historyTableName,
@@ -3793,6 +3864,8 @@ var HistoryCleanupManager = class {
3793
3864
  const organizationId = this.dbLoader.organizationId;
3794
3865
  let recordsByAge = 0;
3795
3866
  let recordsByCount = 0;
3867
+ const pinnedTypes = executionPinnedTypes();
3868
+ const isPinned = (t) => !!t && pinnedTypes.includes(t);
3796
3869
  try {
3797
3870
  const baseWhere = {};
3798
3871
  if (organizationId) baseWhere.organization_id = organizationId;
@@ -3804,25 +3877,31 @@ var HistoryCleanupManager = class {
3804
3877
  recorded_at: { $lt: cutoffISO },
3805
3878
  ...baseWhere
3806
3879
  };
3880
+ if (pinnedTypes.length > 0) {
3881
+ filter.type = { $nin: pinnedTypes };
3882
+ }
3807
3883
  recordsByAge = await driver.count(historyTableName, {
3808
3884
  object: historyTableName,
3809
3885
  where: filter
3810
3886
  });
3811
3887
  }
3812
3888
  if (this.policy.maxVersions) {
3813
- const metadataIds = await driver.find(historyTableName, {
3889
+ const metaItems = await driver.find(historyTableName, {
3814
3890
  object: historyTableName,
3815
3891
  where: baseWhere,
3816
- fields: ["metadata_id"]
3892
+ fields: ["type", "name"]
3817
3893
  });
3818
- const uniqueIds = /* @__PURE__ */ new Set();
3819
- for (const record of metadataIds) {
3820
- if (record.metadata_id) {
3821
- uniqueIds.add(record.metadata_id);
3894
+ const uniqueKeys = /* @__PURE__ */ new Set();
3895
+ for (const record of metaItems) {
3896
+ const t = record.type;
3897
+ const n = record.name;
3898
+ if (t && n && !isPinned(t)) {
3899
+ uniqueKeys.add(`${t}${n}`);
3822
3900
  }
3823
3901
  }
3824
- for (const metadataId of uniqueIds) {
3825
- const filter = { metadata_id: metadataId, ...baseWhere };
3902
+ for (const key of uniqueKeys) {
3903
+ const [type, name] = key.split("");
3904
+ const filter = { type, name, ...baseWhere };
3826
3905
  const count = await driver.count(historyTableName, {
3827
3906
  object: historyTableName,
3828
3907
  where: filter