@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.d.cts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as System from '@objectstack/spec/system';
2
2
  import { MetadataFormat, MetadataLoaderContract, MetadataLoadOptions, MetadataLoadResult, MetadataStats, MetadataSaveOptions, MetadataSaveResult, MetadataWatchEvent, MetadataManagerConfig, PackagePublishResult, MetadataHistoryQueryOptions, MetadataHistoryQueryResult, MetadataDiffResult, MetadataHistoryRecord, MetadataHistoryRetentionPolicy } from '@objectstack/spec/system';
3
3
  export { MetadataCollectionInfo, MetadataDiffResult, MetadataExportOptions, MetadataFormat, MetadataHistoryQueryOptions, MetadataHistoryQueryResult, MetadataHistoryRecord, MetadataHistoryRetentionPolicy, MetadataImportOptions, MetadataLoadOptions, MetadataLoadResult, MetadataLoaderContract, MetadataManagerConfig, MetadataSaveOptions, MetadataSaveResult, MetadataStats, MetadataWatchEvent } from '@objectstack/spec/system';
4
- import { IMetadataService, IDataDriver, IDataEngine, IRealtimeService, MetadataWatchCallback, MetadataWatchHandle, MetadataExportOptions, MetadataImportOptions, MetadataImportResult, MetadataTypeInfo, ISchemaDriver } from '@objectstack/spec/contracts';
4
+ import { IMetadataService, IDataDriver, IDataEngine, IRealtimeService, MetadataWatchCallback, MetadataWatchHandle, MetadataExportOptions, MetadataImportOptions, MetadataImportResult, MetadataTypeInfo, IPubSub, ISchemaDriver } from '@objectstack/spec/contracts';
5
5
  export { IMetadataService, MetadataImportResult, MetadataTypeInfo, MetadataWatchCallback, MetadataWatchHandle } from '@objectstack/spec/contracts';
6
6
  import { MetadataTypeRegistryEntry, MetadataQuery, MetadataQueryResult, MetadataBulkResult, MetadataOverlay, MetadataValidationResult, MetadataDependency, MetadataPluginConfig } from '@objectstack/spec/kernel';
7
7
  export { MetadataBulkResult, MetadataDependency, MetadataPluginConfig, MetadataPluginManifest, MetadataQuery, MetadataQueryResult, MetadataType, MetadataTypeRegistryEntry, MetadataValidationResult } from '@objectstack/spec/kernel';
@@ -161,6 +161,10 @@ declare class MetadataManager implements IMetadataService {
161
161
  private listCache;
162
162
  private static readonly LIST_CACHE_TTL_MS;
163
163
  private realtimeService?;
164
+ private clusterPubSub?;
165
+ private clusterNodeId?;
166
+ private clusterUnsubscribe?;
167
+ private static readonly CLUSTER_CHANNEL;
164
168
  protected repository?: MetadataRepository;
165
169
  private repoWatchIter?;
166
170
  private repoWatchClosed;
@@ -434,6 +438,24 @@ declare class MetadataManager implements IMetadataService {
434
438
  /** Translate a repo event to the legacy MetadataWatchEvent + invalidate caches. */
435
439
  private applyRepoEvent;
436
440
  protected notifyWatchers(type: string, event: MetadataWatchEvent): void;
441
+ private notifyWatchersLocal;
442
+ /**
443
+ * Attach a cluster pub/sub transport so metadata-change events fan
444
+ * out to peer nodes and remote events replay into local watchers.
445
+ *
446
+ * The bridge plugin in @objectstack/service-cluster calls this once
447
+ * per kernel boot after both cluster and metadata services are
448
+ * registered. Passing the same MetadataManager twice no-ops; passing
449
+ * a different transport replaces the prior subscription.
450
+ *
451
+ * Pass `nodeId` matching the local cluster's nodeId so loopback
452
+ * suppression works.
453
+ *
454
+ * @returns disposer that unsubscribes from cluster events.
455
+ */
456
+ attachClusterPubSub(pubsub: IPubSub, nodeId: string): () => void;
457
+ /** Tear down cluster wiring. Safe to call multiple times. */
458
+ detachClusterPubSub(): void;
437
459
  /**
438
460
  * Get the database loader for history operations.
439
461
  * Returns undefined if no database loader is configured.
@@ -780,7 +802,6 @@ declare class DatabaseLoader implements MetadataLoader {
780
802
  /**
781
803
  * Create a history record for a metadata change.
782
804
  *
783
- * @param metadataId - The metadata record ID
784
805
  * @param type - Metadata type
785
806
  * @param name - Metadata name
786
807
  * @param version - Version number
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as System from '@objectstack/spec/system';
2
2
  import { MetadataFormat, MetadataLoaderContract, MetadataLoadOptions, MetadataLoadResult, MetadataStats, MetadataSaveOptions, MetadataSaveResult, MetadataWatchEvent, MetadataManagerConfig, PackagePublishResult, MetadataHistoryQueryOptions, MetadataHistoryQueryResult, MetadataDiffResult, MetadataHistoryRecord, MetadataHistoryRetentionPolicy } from '@objectstack/spec/system';
3
3
  export { MetadataCollectionInfo, MetadataDiffResult, MetadataExportOptions, MetadataFormat, MetadataHistoryQueryOptions, MetadataHistoryQueryResult, MetadataHistoryRecord, MetadataHistoryRetentionPolicy, MetadataImportOptions, MetadataLoadOptions, MetadataLoadResult, MetadataLoaderContract, MetadataManagerConfig, MetadataSaveOptions, MetadataSaveResult, MetadataStats, MetadataWatchEvent } from '@objectstack/spec/system';
4
- import { IMetadataService, IDataDriver, IDataEngine, IRealtimeService, MetadataWatchCallback, MetadataWatchHandle, MetadataExportOptions, MetadataImportOptions, MetadataImportResult, MetadataTypeInfo, ISchemaDriver } from '@objectstack/spec/contracts';
4
+ import { IMetadataService, IDataDriver, IDataEngine, IRealtimeService, MetadataWatchCallback, MetadataWatchHandle, MetadataExportOptions, MetadataImportOptions, MetadataImportResult, MetadataTypeInfo, IPubSub, ISchemaDriver } from '@objectstack/spec/contracts';
5
5
  export { IMetadataService, MetadataImportResult, MetadataTypeInfo, MetadataWatchCallback, MetadataWatchHandle } from '@objectstack/spec/contracts';
6
6
  import { MetadataTypeRegistryEntry, MetadataQuery, MetadataQueryResult, MetadataBulkResult, MetadataOverlay, MetadataValidationResult, MetadataDependency, MetadataPluginConfig } from '@objectstack/spec/kernel';
7
7
  export { MetadataBulkResult, MetadataDependency, MetadataPluginConfig, MetadataPluginManifest, MetadataQuery, MetadataQueryResult, MetadataType, MetadataTypeRegistryEntry, MetadataValidationResult } from '@objectstack/spec/kernel';
@@ -161,6 +161,10 @@ declare class MetadataManager implements IMetadataService {
161
161
  private listCache;
162
162
  private static readonly LIST_CACHE_TTL_MS;
163
163
  private realtimeService?;
164
+ private clusterPubSub?;
165
+ private clusterNodeId?;
166
+ private clusterUnsubscribe?;
167
+ private static readonly CLUSTER_CHANNEL;
164
168
  protected repository?: MetadataRepository;
165
169
  private repoWatchIter?;
166
170
  private repoWatchClosed;
@@ -434,6 +438,24 @@ declare class MetadataManager implements IMetadataService {
434
438
  /** Translate a repo event to the legacy MetadataWatchEvent + invalidate caches. */
435
439
  private applyRepoEvent;
436
440
  protected notifyWatchers(type: string, event: MetadataWatchEvent): void;
441
+ private notifyWatchersLocal;
442
+ /**
443
+ * Attach a cluster pub/sub transport so metadata-change events fan
444
+ * out to peer nodes and remote events replay into local watchers.
445
+ *
446
+ * The bridge plugin in @objectstack/service-cluster calls this once
447
+ * per kernel boot after both cluster and metadata services are
448
+ * registered. Passing the same MetadataManager twice no-ops; passing
449
+ * a different transport replaces the prior subscription.
450
+ *
451
+ * Pass `nodeId` matching the local cluster's nodeId so loopback
452
+ * suppression works.
453
+ *
454
+ * @returns disposer that unsubscribes from cluster events.
455
+ */
456
+ attachClusterPubSub(pubsub: IPubSub, nodeId: string): () => void;
457
+ /** Tear down cluster wiring. Safe to call multiple times. */
458
+ detachClusterPubSub(): void;
437
459
  /**
438
460
  * Get the database loader for history operations.
439
461
  * Returns undefined if no database loader is configured.
@@ -780,7 +802,6 @@ declare class DatabaseLoader implements MetadataLoader {
780
802
  /**
781
803
  * Create a history record for a metadata change.
782
804
  *
783
- * @param metadataId - The metadata record ID
784
805
  * @param type - Metadata type
785
806
  * @param name - Metadata name
786
807
  * @param version - Version number
package/dist/index.js CHANGED
@@ -755,7 +755,6 @@ var DatabaseLoader = class {
755
755
  /**
756
756
  * Create a history record for a metadata change.
757
757
  *
758
- * @param metadataId - The metadata record ID
759
758
  * @param type - Metadata type
760
759
  * @param name - Metadata name
761
760
  * @param version - Version number
@@ -765,7 +764,7 @@ var DatabaseLoader = class {
765
764
  * @param changeNote - Optional change description
766
765
  * @param recordedBy - Optional user who made the change
767
766
  */
768
- async createHistoryRecord(metadataId, type, name, version, metadata, operationType, previousChecksum, changeNote, recordedBy) {
767
+ async createHistoryRecord(type, name, version, metadata, operationType, previousChecksum, changeNote, recordedBy) {
769
768
  if (!this.trackHistory) return;
770
769
  await this.ensureHistorySchema();
771
770
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -778,7 +777,6 @@ var DatabaseLoader = class {
778
777
  const eventSeq = await this.nextEventSeq();
779
778
  const historyRecord = {
780
779
  id: historyId,
781
- metadataId,
782
780
  name,
783
781
  type,
784
782
  version,
@@ -795,7 +793,6 @@ var DatabaseLoader = class {
795
793
  await this._create(this.historyTableName, {
796
794
  id: historyRecord.id,
797
795
  event_seq: eventSeq,
798
- metadata_id: historyRecord.metadataId,
799
796
  name: historyRecord.name,
800
797
  type: historyRecord.type,
801
798
  version: historyRecord.version,
@@ -983,12 +980,9 @@ var DatabaseLoader = class {
983
980
  async getHistoryRecord(type, name, version) {
984
981
  if (!this.trackHistory) return null;
985
982
  await this.ensureHistorySchema();
986
- const metadataRow = await this._findOne(this.tableName, {
987
- where: this.baseFilter(type, name)
988
- });
989
- if (!metadataRow) return null;
990
983
  const filter = {
991
- metadata_id: metadataRow.id,
984
+ type,
985
+ name,
992
986
  version
993
987
  };
994
988
  if (this.organizationId) {
@@ -1000,7 +994,6 @@ var DatabaseLoader = class {
1000
994
  if (!row) return null;
1001
995
  return {
1002
996
  id: row.id,
1003
- metadataId: row.metadata_id,
1004
997
  name: row.name,
1005
998
  type: row.type,
1006
999
  version: row.version,
@@ -1025,14 +1018,9 @@ var DatabaseLoader = class {
1025
1018
  }
1026
1019
  await this.ensureSchema();
1027
1020
  await this.ensureHistorySchema();
1028
- const filter = { type, name };
1029
- if (this.organizationId) filter.organization_id = this.organizationId;
1030
- const metadataRecord = await this._findOne(this.tableName, { where: filter });
1031
- if (!metadataRecord) {
1032
- return { records: [], total: 0, hasMore: false };
1033
- }
1034
1021
  const historyFilter = {
1035
- metadata_id: metadataRecord.id
1022
+ type,
1023
+ name
1036
1024
  };
1037
1025
  if (this.organizationId) historyFilter.organization_id = this.organizationId;
1038
1026
  if (options?.operationType) historyFilter.operation_type = options.operationType;
@@ -1063,7 +1051,6 @@ var DatabaseLoader = class {
1063
1051
  const parsedMetadata = typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata;
1064
1052
  return {
1065
1053
  id: row.id,
1066
- metadataId: row.metadata_id,
1067
1054
  name: row.name,
1068
1055
  type: row.type,
1069
1056
  version: row.version,
@@ -1108,7 +1095,6 @@ var DatabaseLoader = class {
1108
1095
  });
1109
1096
  this.invalidate(type, name);
1110
1097
  await this.createHistoryRecord(
1111
- existing.id,
1112
1098
  type,
1113
1099
  name,
1114
1100
  newVersion,
@@ -1150,7 +1136,6 @@ var DatabaseLoader = class {
1150
1136
  });
1151
1137
  this.invalidate(type, name);
1152
1138
  await this.createHistoryRecord(
1153
- existing.id,
1154
1139
  type,
1155
1140
  name,
1156
1141
  version,
@@ -1184,7 +1169,6 @@ var DatabaseLoader = class {
1184
1169
  });
1185
1170
  this.invalidate(type, name);
1186
1171
  await this.createHistoryRecord(
1187
- id,
1188
1172
  type,
1189
1173
  name,
1190
1174
  1,
@@ -2364,6 +2348,23 @@ var _MetadataManager = class _MetadataManager {
2364
2348
  this.notifyWatchers(type, legacyEvent);
2365
2349
  }
2366
2350
  notifyWatchers(type, event) {
2351
+ this.notifyWatchersLocal(type, event);
2352
+ if (this.clusterPubSub) {
2353
+ const payload = {
2354
+ originNode: this.clusterNodeId,
2355
+ type,
2356
+ event
2357
+ };
2358
+ const key = `${type}:${event.name ?? ""}`;
2359
+ void this.clusterPubSub.publish(_MetadataManager.CLUSTER_CHANNEL, payload, { partitionKey: key }).catch((err) => {
2360
+ this.logger.error("Cluster metadata publish failed", void 0, {
2361
+ type,
2362
+ error: err instanceof Error ? err.message : String(err)
2363
+ });
2364
+ });
2365
+ }
2366
+ }
2367
+ notifyWatchersLocal(type, event) {
2367
2368
  const callbacks = this.watchCallbacks.get(type);
2368
2369
  if (!callbacks) return;
2369
2370
  for (const callback of callbacks) {
@@ -2377,6 +2378,63 @@ var _MetadataManager = class _MetadataManager {
2377
2378
  }
2378
2379
  }
2379
2380
  }
2381
+ /**
2382
+ * Attach a cluster pub/sub transport so metadata-change events fan
2383
+ * out to peer nodes and remote events replay into local watchers.
2384
+ *
2385
+ * The bridge plugin in @objectstack/service-cluster calls this once
2386
+ * per kernel boot after both cluster and metadata services are
2387
+ * registered. Passing the same MetadataManager twice no-ops; passing
2388
+ * a different transport replaces the prior subscription.
2389
+ *
2390
+ * Pass `nodeId` matching the local cluster's nodeId so loopback
2391
+ * suppression works.
2392
+ *
2393
+ * @returns disposer that unsubscribes from cluster events.
2394
+ */
2395
+ attachClusterPubSub(pubsub, nodeId) {
2396
+ if (this.clusterPubSub === pubsub && this.clusterNodeId === nodeId) {
2397
+ return () => this.detachClusterPubSub();
2398
+ }
2399
+ this.detachClusterPubSub();
2400
+ this.clusterPubSub = pubsub;
2401
+ this.clusterNodeId = nodeId;
2402
+ this.clusterUnsubscribe = pubsub.subscribe(
2403
+ _MetadataManager.CLUSTER_CHANNEL,
2404
+ (msg) => {
2405
+ const p = msg.payload;
2406
+ if (p?.originNode && p.originNode === this.clusterNodeId) return;
2407
+ if (!p?.type || !p.event) return;
2408
+ setImmediate(() => {
2409
+ try {
2410
+ this.notifyWatchersLocal(p.type, p.event);
2411
+ } catch (err) {
2412
+ this.logger.error("Cluster remote replay failed", void 0, {
2413
+ type: p.type,
2414
+ error: err instanceof Error ? err.message : String(err)
2415
+ });
2416
+ }
2417
+ });
2418
+ }
2419
+ );
2420
+ this.logger.info("MetadataManager attached to cluster pubsub", {
2421
+ nodeId,
2422
+ channel: _MetadataManager.CLUSTER_CHANNEL
2423
+ });
2424
+ return () => this.detachClusterPubSub();
2425
+ }
2426
+ /** Tear down cluster wiring. Safe to call multiple times. */
2427
+ detachClusterPubSub() {
2428
+ if (this.clusterUnsubscribe) {
2429
+ try {
2430
+ this.clusterUnsubscribe();
2431
+ } catch {
2432
+ }
2433
+ this.clusterUnsubscribe = void 0;
2434
+ }
2435
+ this.clusterPubSub = void 0;
2436
+ this.clusterNodeId = void 0;
2437
+ }
2380
2438
  // ==========================================
2381
2439
  // Version History & Rollback
2382
2440
  // ==========================================
@@ -2477,6 +2535,7 @@ var _MetadataManager = class _MetadataManager {
2477
2535
  }
2478
2536
  };
2479
2537
  _MetadataManager.LIST_CACHE_TTL_MS = 3e4;
2538
+ _MetadataManager.CLUSTER_CHANNEL = "metadata.changed";
2480
2539
  var MetadataManager = _MetadataManager;
2481
2540
 
2482
2541
  // src/plugin.ts
@@ -3603,6 +3662,10 @@ function registerMetadataHistoryRoutes(app, metadataService) {
3603
3662
  }
3604
3663
 
3605
3664
  // src/utils/history-cleanup.ts
3665
+ import { DEFAULT_METADATA_TYPE_REGISTRY as DEFAULT_METADATA_TYPE_REGISTRY2 } from "@objectstack/spec/kernel";
3666
+ function executionPinnedTypes() {
3667
+ return DEFAULT_METADATA_TYPE_REGISTRY2.filter((entry) => entry.executionPinned).map((entry) => entry.type);
3668
+ }
3606
3669
  var HistoryCleanupManager = class {
3607
3670
  constructor(policy, dbLoader) {
3608
3671
  this.policy = policy;
@@ -3640,6 +3703,8 @@ var HistoryCleanupManager = class {
3640
3703
  const organizationId = this.dbLoader.organizationId;
3641
3704
  let deleted = 0;
3642
3705
  let errors = 0;
3706
+ const pinnedTypes = executionPinnedTypes();
3707
+ const isPinned = (t) => !!t && pinnedTypes.includes(t);
3643
3708
  try {
3644
3709
  if (this.policy.maxAgeDays) {
3645
3710
  const cutoffDate = /* @__PURE__ */ new Date();
@@ -3651,6 +3716,9 @@ var HistoryCleanupManager = class {
3651
3716
  if (organizationId) {
3652
3717
  filter.organization_id = organizationId;
3653
3718
  }
3719
+ if (pinnedTypes.length > 0) {
3720
+ filter.type = { $nin: pinnedTypes };
3721
+ }
3654
3722
  try {
3655
3723
  const result = await this.bulkDeleteByFilter(driver, historyTableName, filter);
3656
3724
  deleted += result.deleted;
@@ -3663,19 +3731,22 @@ var HistoryCleanupManager = class {
3663
3731
  try {
3664
3732
  const baseWhere = {};
3665
3733
  if (organizationId) baseWhere.organization_id = organizationId;
3666
- const metadataIds = await driver.find(historyTableName, {
3734
+ const metaItems = await driver.find(historyTableName, {
3667
3735
  object: historyTableName,
3668
3736
  where: baseWhere,
3669
- fields: ["metadata_id"]
3737
+ fields: ["type", "name"]
3670
3738
  });
3671
- const uniqueIds = /* @__PURE__ */ new Set();
3672
- for (const record of metadataIds) {
3673
- if (record.metadata_id) {
3674
- uniqueIds.add(record.metadata_id);
3739
+ const uniqueKeys = /* @__PURE__ */ new Set();
3740
+ for (const record of metaItems) {
3741
+ const t = record.type;
3742
+ const n = record.name;
3743
+ if (t && n && !isPinned(t)) {
3744
+ uniqueKeys.add(`${t}${n}`);
3675
3745
  }
3676
3746
  }
3677
- for (const metadataId of uniqueIds) {
3678
- const filter = { metadata_id: metadataId, ...baseWhere };
3747
+ for (const key of uniqueKeys) {
3748
+ const [type, name] = key.split("");
3749
+ const filter = { type, name, ...baseWhere };
3679
3750
  try {
3680
3751
  const historyRecords = await driver.find(historyTableName, {
3681
3752
  object: historyTableName,
@@ -3752,6 +3823,8 @@ var HistoryCleanupManager = class {
3752
3823
  const organizationId = this.dbLoader.organizationId;
3753
3824
  let recordsByAge = 0;
3754
3825
  let recordsByCount = 0;
3826
+ const pinnedTypes = executionPinnedTypes();
3827
+ const isPinned = (t) => !!t && pinnedTypes.includes(t);
3755
3828
  try {
3756
3829
  const baseWhere = {};
3757
3830
  if (organizationId) baseWhere.organization_id = organizationId;
@@ -3763,25 +3836,31 @@ var HistoryCleanupManager = class {
3763
3836
  recorded_at: { $lt: cutoffISO },
3764
3837
  ...baseWhere
3765
3838
  };
3839
+ if (pinnedTypes.length > 0) {
3840
+ filter.type = { $nin: pinnedTypes };
3841
+ }
3766
3842
  recordsByAge = await driver.count(historyTableName, {
3767
3843
  object: historyTableName,
3768
3844
  where: filter
3769
3845
  });
3770
3846
  }
3771
3847
  if (this.policy.maxVersions) {
3772
- const metadataIds = await driver.find(historyTableName, {
3848
+ const metaItems = await driver.find(historyTableName, {
3773
3849
  object: historyTableName,
3774
3850
  where: baseWhere,
3775
- fields: ["metadata_id"]
3851
+ fields: ["type", "name"]
3776
3852
  });
3777
- const uniqueIds = /* @__PURE__ */ new Set();
3778
- for (const record of metadataIds) {
3779
- if (record.metadata_id) {
3780
- uniqueIds.add(record.metadata_id);
3853
+ const uniqueKeys = /* @__PURE__ */ new Set();
3854
+ for (const record of metaItems) {
3855
+ const t = record.type;
3856
+ const n = record.name;
3857
+ if (t && n && !isPinned(t)) {
3858
+ uniqueKeys.add(`${t}${n}`);
3781
3859
  }
3782
3860
  }
3783
- for (const metadataId of uniqueIds) {
3784
- const filter = { metadata_id: metadataId, ...baseWhere };
3861
+ for (const key of uniqueKeys) {
3862
+ const [type, name] = key.split("");
3863
+ const filter = { type, name, ...baseWhere };
3785
3864
  const count = await driver.count(historyTableName, {
3786
3865
  object: historyTableName,
3787
3866
  where: filter