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