@objectstack/metadata 4.2.0 → 5.1.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.d.cts CHANGED
@@ -5,6 +5,7 @@ import { MetadataLoaderContract, MetadataFormat, MetadataLoadOptions, MetadataLo
5
5
  export { MetadataCollectionInfo, MetadataDiffResult, MetadataExportOptions, MetadataFormat, MetadataHistoryQueryOptions, MetadataHistoryQueryResult, MetadataHistoryRecord, MetadataHistoryRetentionPolicy, MetadataImportOptions, MetadataLoadOptions, MetadataLoadResult, MetadataLoaderContract, MetadataManagerConfig, MetadataSaveOptions, MetadataSaveResult, MetadataStats, MetadataWatchEvent } from '@objectstack/spec/system';
6
6
  export { IMetadataService, MetadataImportResult, MetadataTypeInfo, MetadataWatchCallback, MetadataWatchHandle } from '@objectstack/spec/contracts';
7
7
  export { MetadataBulkResult, MetadataDependency, MetadataPluginConfig, MetadataPluginManifest, MetadataQuery, MetadataQueryResult, MetadataType, MetadataTypeRegistryEntry, MetadataValidationResult } from '@objectstack/spec/kernel';
8
+ export { HistoryOptions, MetaRef, MetadataEvent, MetadataItem, MetadataItemHeader, MetadataRepository, WatchFilter } from '@objectstack/metadata-core';
8
9
  import { Logger } from '@objectstack/core';
9
10
  import 'zod';
10
11
 
package/dist/node.d.ts CHANGED
@@ -5,6 +5,7 @@ import { MetadataLoaderContract, MetadataFormat, MetadataLoadOptions, MetadataLo
5
5
  export { MetadataCollectionInfo, MetadataDiffResult, MetadataExportOptions, MetadataFormat, MetadataHistoryQueryOptions, MetadataHistoryQueryResult, MetadataHistoryRecord, MetadataHistoryRetentionPolicy, MetadataImportOptions, MetadataLoadOptions, MetadataLoadResult, MetadataLoaderContract, MetadataManagerConfig, MetadataSaveOptions, MetadataSaveResult, MetadataStats, MetadataWatchEvent } from '@objectstack/spec/system';
6
6
  export { IMetadataService, MetadataImportResult, MetadataTypeInfo, MetadataWatchCallback, MetadataWatchHandle } from '@objectstack/spec/contracts';
7
7
  export { MetadataBulkResult, MetadataDependency, MetadataPluginConfig, MetadataPluginManifest, MetadataQuery, MetadataQueryResult, MetadataType, MetadataTypeRegistryEntry, MetadataValidationResult } from '@objectstack/spec/kernel';
8
+ export { HistoryOptions, MetaRef, MetadataEvent, MetadataItem, MetadataItemHeader, MetadataRepository, WatchFilter } from '@objectstack/metadata-core';
8
9
  import { Logger } from '@objectstack/core';
9
10
  import 'zod';
10
11
 
package/dist/node.js CHANGED
@@ -42,7 +42,12 @@ function registerMetadataHmrRoutes(app, manager, options = {}) {
42
42
  metadataType: evt.metadataType ?? type,
43
43
  name: evt.name ?? "",
44
44
  path: evt.path,
45
- timestamp: Number.isFinite(ts) ? ts : Date.now()
45
+ timestamp: Number.isFinite(ts) ? ts : Date.now(),
46
+ // Forward the canonical server-side sequence number when the
47
+ // event originated from a MetadataRepository (ADR-0008). Legacy
48
+ // chokidar-driven events have no seq — clients fall back to
49
+ // their local counter in that case.
50
+ ...typeof evt.seq === "number" ? { seq: evt.seq } : {}
46
51
  });
47
52
  });
48
53
  }
@@ -559,7 +564,7 @@ var DatabaseLoader = class {
559
564
  this.tableName = options.tableName ?? "sys_metadata";
560
565
  this.historyTableName = options.historyTableName ?? "sys_metadata_history";
561
566
  this.organizationId = options.organizationId;
562
- this.projectId = options.projectId;
567
+ void options.projectId;
563
568
  this.trackHistory = options.trackHistory !== false;
564
569
  const cacheOpts = options.cache;
565
570
  const cacheEnabled = cacheOpts?.enabled !== false;
@@ -649,6 +654,26 @@ var DatabaseLoader = class {
649
654
  }
650
655
  return this.driver.delete(table, id);
651
656
  }
657
+ /**
658
+ * Compute the next per-org `event_seq` for `sys_metadata_history`.
659
+ * Reads `MAX(event_seq) + 1` for the configured `organization_id`.
660
+ * Legacy path — not transactional, so concurrent writes can collide.
661
+ * The canonical (transactional) producer is `SysMetadataRepository`.
662
+ */
663
+ async nextEventSeq() {
664
+ const where = this.organizationId ? { organization_id: this.organizationId } : {};
665
+ try {
666
+ const rows = await this._find(this.historyTableName, { where });
667
+ let max = 0;
668
+ for (const row of rows) {
669
+ const v = typeof row.event_seq === "number" ? row.event_seq : 0;
670
+ if (v > max) max = v;
671
+ }
672
+ return max + 1;
673
+ } catch {
674
+ return 1;
675
+ }
676
+ }
652
677
  /**
653
678
  * Ensure the metadata table exists.
654
679
  * Uses IDataDriver.syncSchema with the SysMetadataObject definition
@@ -713,8 +738,9 @@ var DatabaseLoader = class {
713
738
  }
714
739
  /**
715
740
  * Build base filter conditions for queries.
716
- * Filters by organizationId when configured; project_id when projectId is set,
717
- * or null (platform-global) when not set.
741
+ * Filters by organizationId when configured. `projectId` is accepted
742
+ * for back-compat but no longer constrains the query — see
743
+ * ADR-0008 §0 (branch/project removal).
718
744
  */
719
745
  baseFilter(type, name) {
720
746
  const filter = { type };
@@ -724,13 +750,11 @@ var DatabaseLoader = class {
724
750
  if (this.organizationId) {
725
751
  filter.organization_id = this.organizationId;
726
752
  }
727
- filter.project_id = this.projectId ?? null;
728
753
  return filter;
729
754
  }
730
755
  /**
731
756
  * Create a history record for a metadata change.
732
757
  *
733
- * @param metadataId - The metadata record ID
734
758
  * @param type - Metadata type
735
759
  * @param name - Metadata name
736
760
  * @param version - Version number
@@ -740,7 +764,7 @@ var DatabaseLoader = class {
740
764
  * @param changeNote - Optional change description
741
765
  * @param recordedBy - Optional user who made the change
742
766
  */
743
- async createHistoryRecord(metadataId, type, name, version, metadata, operationType, previousChecksum, changeNote, recordedBy) {
767
+ async createHistoryRecord(type, name, version, metadata, operationType, previousChecksum, changeNote, recordedBy) {
744
768
  if (!this.trackHistory) return;
745
769
  await this.ensureHistorySchema();
746
770
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -750,9 +774,9 @@ var DatabaseLoader = class {
750
774
  }
751
775
  const historyId = generateId();
752
776
  const metadataJson = JSON.stringify(metadata);
777
+ const eventSeq = await this.nextEventSeq();
753
778
  const historyRecord = {
754
779
  id: historyId,
755
- metadataId,
756
780
  name,
757
781
  type,
758
782
  version,
@@ -763,13 +787,12 @@ var DatabaseLoader = class {
763
787
  changeNote,
764
788
  recordedBy,
765
789
  recordedAt: now,
766
- ...this.organizationId ? { organizationId: this.organizationId } : {},
767
- ...this.projectId !== void 0 ? { projectId: this.projectId } : {}
790
+ ...this.organizationId ? { organizationId: this.organizationId } : {}
768
791
  };
769
792
  try {
770
793
  await this._create(this.historyTableName, {
771
794
  id: historyRecord.id,
772
- metadata_id: historyRecord.metadataId,
795
+ event_seq: eventSeq,
773
796
  name: historyRecord.name,
774
797
  type: historyRecord.type,
775
798
  version: historyRecord.version,
@@ -780,8 +803,8 @@ var DatabaseLoader = class {
780
803
  change_note: historyRecord.changeNote,
781
804
  recorded_by: historyRecord.recordedBy,
782
805
  recorded_at: historyRecord.recordedAt,
783
- ...this.organizationId ? { organization_id: this.organizationId } : {},
784
- ...this.projectId !== void 0 ? { project_id: this.projectId } : {}
806
+ source: "database-loader",
807
+ ...this.organizationId ? { organization_id: this.organizationId } : {}
785
808
  });
786
809
  } catch (error) {
787
810
  console.error(`Failed to create history record for ${type}/${name}:`, error);
@@ -957,25 +980,20 @@ var DatabaseLoader = class {
957
980
  async getHistoryRecord(type, name, version) {
958
981
  if (!this.trackHistory) return null;
959
982
  await this.ensureHistorySchema();
960
- const metadataRow = await this._findOne(this.tableName, {
961
- where: this.baseFilter(type, name)
962
- });
963
- if (!metadataRow) return null;
964
983
  const filter = {
965
- metadata_id: metadataRow.id,
984
+ type,
985
+ name,
966
986
  version
967
987
  };
968
988
  if (this.organizationId) {
969
989
  filter.organization_id = this.organizationId;
970
990
  }
971
- filter.project_id = this.projectId ?? null;
972
991
  const row = await this._findOne(this.historyTableName, {
973
992
  where: filter
974
993
  });
975
994
  if (!row) return null;
976
995
  return {
977
996
  id: row.id,
978
- metadataId: row.metadata_id,
979
997
  name: row.name,
980
998
  type: row.type,
981
999
  version: row.version,
@@ -985,7 +1003,6 @@ var DatabaseLoader = class {
985
1003
  previousChecksum: row.previous_checksum,
986
1004
  changeNote: row.change_note,
987
1005
  organizationId: row.organization_id,
988
- projectId: row.project_id,
989
1006
  recordedBy: row.recorded_by,
990
1007
  recordedAt: row.recorded_at
991
1008
  };
@@ -1001,18 +1018,11 @@ var DatabaseLoader = class {
1001
1018
  }
1002
1019
  await this.ensureSchema();
1003
1020
  await this.ensureHistorySchema();
1004
- const filter = { type, name };
1005
- if (this.organizationId) filter.organization_id = this.organizationId;
1006
- filter.project_id = this.projectId ?? null;
1007
- const metadataRecord = await this._findOne(this.tableName, { where: filter });
1008
- if (!metadataRecord) {
1009
- return { records: [], total: 0, hasMore: false };
1010
- }
1011
1021
  const historyFilter = {
1012
- metadata_id: metadataRecord.id
1022
+ type,
1023
+ name
1013
1024
  };
1014
1025
  if (this.organizationId) historyFilter.organization_id = this.organizationId;
1015
- historyFilter.project_id = this.projectId ?? null;
1016
1026
  if (options?.operationType) historyFilter.operation_type = options.operationType;
1017
1027
  if (options?.since) historyFilter.recorded_at = { $gte: options.since };
1018
1028
  if (options?.until) {
@@ -1041,7 +1051,6 @@ var DatabaseLoader = class {
1041
1051
  const parsedMetadata = typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata;
1042
1052
  return {
1043
1053
  id: row.id,
1044
- metadataId: row.metadata_id,
1045
1054
  name: row.name,
1046
1055
  type: row.type,
1047
1056
  version: row.version,
@@ -1051,7 +1060,6 @@ var DatabaseLoader = class {
1051
1060
  previousChecksum: row.previous_checksum,
1052
1061
  changeNote: row.change_note,
1053
1062
  organizationId: row.organization_id,
1054
- projectId: row.project_id,
1055
1063
  recordedBy: row.recorded_by,
1056
1064
  recordedAt: row.recorded_at
1057
1065
  };
@@ -1087,7 +1095,6 @@ var DatabaseLoader = class {
1087
1095
  });
1088
1096
  this.invalidate(type, name);
1089
1097
  await this.createHistoryRecord(
1090
- existing.id,
1091
1098
  type,
1092
1099
  name,
1093
1100
  newVersion,
@@ -1129,7 +1136,6 @@ var DatabaseLoader = class {
1129
1136
  });
1130
1137
  this.invalidate(type, name);
1131
1138
  await this.createHistoryRecord(
1132
- existing.id,
1133
1139
  type,
1134
1140
  name,
1135
1141
  version,
@@ -1158,13 +1164,11 @@ var DatabaseLoader = class {
1158
1164
  version: 1,
1159
1165
  source: "database",
1160
1166
  ...this.organizationId ? { organization_id: this.organizationId } : {},
1161
- ...this.projectId !== void 0 ? { project_id: this.projectId } : { project_id: null },
1162
1167
  created_at: now,
1163
1168
  updated_at: now
1164
1169
  });
1165
1170
  this.invalidate(type, name);
1166
1171
  await this.createHistoryRecord(
1167
- id,
1168
1172
  type,
1169
1173
  name,
1170
1174
  1,
@@ -1231,6 +1235,7 @@ var _MetadataManager = class _MetadataManager {
1231
1235
  // Invalidated on every `register()` / `unregister()` to keep CRUD writes
1232
1236
  // visible to subsequent reads.
1233
1237
  this.listCache = /* @__PURE__ */ new Map();
1238
+ this.repoWatchClosed = false;
1234
1239
  this.config = config;
1235
1240
  this.logger = createLogger({ level: "info", format: "pretty" });
1236
1241
  this.serializers = /* @__PURE__ */ new Map();
@@ -2238,6 +2243,110 @@ var _MetadataManager = class _MetadataManager {
2238
2243
  */
2239
2244
  async stopWatching() {
2240
2245
  }
2246
+ // ─── ADR-0008 PR-6: Repository wiring ───────────────────────────────
2247
+ /**
2248
+ * Attach a {@link MetadataRepository} as a supplementary event source.
2249
+ *
2250
+ * The manager subscribes to `repo.watch({})` and re-emits each event
2251
+ * through {@link notifyWatchers} as a legacy `MetadataWatchEvent`.
2252
+ * Each event also invalidates the in-memory registry entry and the
2253
+ * `list()` cache for the affected type so subsequent reads see fresh
2254
+ * data.
2255
+ *
2256
+ * No write-through. `register()` / `unregister()` / `save()` are
2257
+ * untouched in this PR (deferred to ADR-0008 M0 PR-10).
2258
+ *
2259
+ * Call {@link dispose} (or {@link stopRepositoryWatch}) to detach.
2260
+ */
2261
+ setRepository(repo) {
2262
+ if (this.repository === repo) return;
2263
+ if (this.repository) {
2264
+ void this.stopRepositoryWatch();
2265
+ }
2266
+ this.repository = repo;
2267
+ this.repoWatchClosed = false;
2268
+ void this.startRepositoryWatch();
2269
+ }
2270
+ /** Return the attached repository, if any. */
2271
+ getRepository() {
2272
+ return this.repository;
2273
+ }
2274
+ /** Stop the active repo.watch() loop (best-effort). */
2275
+ async stopRepositoryWatch() {
2276
+ this.repoWatchClosed = true;
2277
+ const iter = this.repoWatchIter;
2278
+ this.repoWatchIter = void 0;
2279
+ if (iter && typeof iter.return === "function") {
2280
+ try {
2281
+ await iter.return(void 0);
2282
+ } catch {
2283
+ }
2284
+ }
2285
+ }
2286
+ /**
2287
+ * Best-effort cleanup. Stops the FS watcher (if any), drains the
2288
+ * repository watch loop, and clears registry caches. Safe to call
2289
+ * multiple times.
2290
+ */
2291
+ async dispose() {
2292
+ await this.stopWatching().catch(() => void 0);
2293
+ await this.stopRepositoryWatch().catch(() => void 0);
2294
+ this.listCache.clear();
2295
+ }
2296
+ async startRepositoryWatch() {
2297
+ const repo = this.repository;
2298
+ if (!repo) return;
2299
+ const iterable = repo.watch({});
2300
+ const iter = iterable[Symbol.asyncIterator]();
2301
+ this.repoWatchIter = iter;
2302
+ try {
2303
+ while (!this.repoWatchClosed) {
2304
+ const { value, done } = await iter.next();
2305
+ if (done) break;
2306
+ try {
2307
+ this.applyRepoEvent(value);
2308
+ } catch (err) {
2309
+ this.logger.warn("[MetadataManager] repo event handler failed", {
2310
+ error: err instanceof Error ? err.message : String(err)
2311
+ });
2312
+ }
2313
+ }
2314
+ } catch (err) {
2315
+ if (!this.repoWatchClosed) {
2316
+ this.logger.warn("[MetadataManager] repository watch loop exited unexpectedly", {
2317
+ error: err instanceof Error ? err.message : String(err)
2318
+ });
2319
+ }
2320
+ } finally {
2321
+ if (this.repoWatchIter === iter) this.repoWatchIter = void 0;
2322
+ }
2323
+ }
2324
+ /** Translate a repo event to the legacy MetadataWatchEvent + invalidate caches. */
2325
+ applyRepoEvent(evt) {
2326
+ const ref = evt.ref;
2327
+ const type = ref.type;
2328
+ const name = ref.name;
2329
+ const typeStore = this.registry.get(type);
2330
+ if (typeStore) {
2331
+ typeStore.delete(name);
2332
+ if (typeStore.size === 0) this.registry.delete(type);
2333
+ }
2334
+ this.listCache.delete(type);
2335
+ const legacyType = evt.op === "create" ? "added" : evt.op === "delete" ? "deleted" : "changed";
2336
+ const legacyEvent = {
2337
+ type: legacyType,
2338
+ metadataType: type,
2339
+ name,
2340
+ path: "",
2341
+ // Repo events carry the hash only; the body is fetched on demand
2342
+ // via manager.get(type, name). HMR consumers don't read `data` so
2343
+ // this is fine for M0. (See ADR-0008 §12 open question 1.)
2344
+ data: void 0,
2345
+ timestamp: evt.ts
2346
+ };
2347
+ legacyEvent.seq = evt.seq;
2348
+ this.notifyWatchers(type, legacyEvent);
2349
+ }
2241
2350
  notifyWatchers(type, event) {
2242
2351
  const callbacks = this.watchCallbacks.get(type);
2243
2352
  if (!callbacks) return;
@@ -2713,7 +2822,13 @@ var NodeMetadataManager = class extends MetadataManager {
2713
2822
  this.watcher = chokidarWatch(rootDir, {
2714
2823
  ignored,
2715
2824
  persistent,
2716
- ignoreInitial: true
2825
+ ignoreInitial: true,
2826
+ // Use polling to avoid `fs.watch` EMFILE on macOS / busy dev hosts.
2827
+ // Recursive watch over a project root would otherwise wire native
2828
+ // watches across the entire tree, easily exhausting the FD pool.
2829
+ usePolling: true,
2830
+ interval: 1e3,
2831
+ binaryInterval: 2e3
2717
2832
  });
2718
2833
  this.watcher.on("add", async (filePath) => {
2719
2834
  await this.handleFileEvent("added", filePath);
@@ -2851,6 +2966,7 @@ var queryableMetadataObjects = [
2851
2966
  SysMetadataObject2,
2852
2967
  SysMetadataHistoryObject2
2853
2968
  ];
2969
+ var REPO_SUBDIR = ".objectstack/metadata";
2854
2970
  var ARTIFACT_FIELD_TO_TYPE = {
2855
2971
  objects: "object",
2856
2972
  objectExtensions: "object_extension",
@@ -2948,6 +3064,31 @@ var MetadataPlugin = class {
2948
3064
  await this._loadFromFileSystem(ctx);
2949
3065
  }
2950
3066
  }
3067
+ const bootstrapMode = this.options.config?.bootstrap ?? "eager";
3068
+ if (bootstrapMode !== "artifact-only") {
3069
+ try {
3070
+ const path3 = await import("path");
3071
+ const { FileSystemRepository } = await import("@objectstack/metadata-fs");
3072
+ const rootDir = this.options.rootDir || process.cwd();
3073
+ const repoRoot = path3.join(rootDir, REPO_SUBDIR);
3074
+ const repo = new FileSystemRepository({
3075
+ root: repoRoot,
3076
+ org: this.options.organizationId ?? "system",
3077
+ disableWatch: this.options.watch === false
3078
+ });
3079
+ await repo.start();
3080
+ this.repository = repo;
3081
+ this.manager.setRepository(repo);
3082
+ ctx.logger.info("[MetadataPlugin] FileSystemRepository attached", {
3083
+ repoRoot,
3084
+ watch: this.options.watch !== false
3085
+ });
3086
+ } catch (e) {
3087
+ ctx.logger.warn("[MetadataPlugin] Failed to attach FileSystemRepository", {
3088
+ error: e?.message
3089
+ });
3090
+ }
3091
+ }
2951
3092
  try {
2952
3093
  const realtimeService = ctx.getService("realtime");
2953
3094
  if (realtimeService && typeof realtimeService === "object" && "publish" in realtimeService) {
@@ -2965,12 +3106,12 @@ var MetadataPlugin = class {
2965
3106
  const { registerMetadataHmrRoutes: registerMetadataHmrRoutes2 } = await Promise.resolve().then(() => (init_hmr_routes(), hmr_routes_exports));
2966
3107
  const hub = registerMetadataHmrRoutes2(httpServer.getRawApp(), this.manager);
2967
3108
  hub.setOnPostReload(async (body = {}) => {
2968
- const src2 = this.options.artifactSource;
2969
- if (src2?.mode === "local-file") {
3109
+ const src3 = this.options.artifactSource;
3110
+ if (src3?.mode === "local-file") {
2970
3111
  try {
2971
- await this._loadFromLocalFile(ctx, src2.path, src2.fetchTimeoutMs);
3112
+ await this._loadFromLocalFile(ctx, src3.path, src3.fetchTimeoutMs);
2972
3113
  ctx.logger.info("[MetadataPlugin] artifact reloaded via HMR POST", {
2973
- path: src2.path,
3114
+ path: src3.path,
2974
3115
  reason: body?.reason
2975
3116
  });
2976
3117
  } catch (e) {
@@ -2979,6 +3120,53 @@ var MetadataPlugin = class {
2979
3120
  }
2980
3121
  }
2981
3122
  });
3123
+ const src2 = this.options.artifactSource;
3124
+ const wantArtifactWatch = this.options.artifactWatch ?? src2?.mode === "local-file";
3125
+ if (src2?.mode === "local-file" && wantArtifactWatch && !/^https?:\/\//i.test(src2.path)) {
3126
+ try {
3127
+ const { watch: chokidarWatch2 } = await import("chokidar");
3128
+ const w = chokidarWatch2(src2.path, {
3129
+ ignoreInitial: true,
3130
+ awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 20 },
3131
+ persistent: true,
3132
+ // Use polling to avoid `fs.watch` exhausting the
3133
+ // process file-descriptor limit on macOS (chokidar
3134
+ // recursively wires watches on the parent
3135
+ // directory tree which can trip EMFILE on busy
3136
+ // dev hosts). 500ms polling is fast enough for
3137
+ // HMR (a recompile takes ~400ms anyway).
3138
+ usePolling: true,
3139
+ interval: 500,
3140
+ binaryInterval: 1e3
3141
+ });
3142
+ let pending = false;
3143
+ const reload = async () => {
3144
+ if (pending) return;
3145
+ pending = true;
3146
+ try {
3147
+ await this._loadFromLocalFile(ctx, src2.path, src2.fetchTimeoutMs);
3148
+ hub.broadcastReload("artifact-file-changed", [src2.path]);
3149
+ ctx.logger.info("[MetadataPlugin] artifact auto-reloaded (file watcher)", {
3150
+ path: src2.path
3151
+ });
3152
+ } catch (e) {
3153
+ ctx.logger.warn("[MetadataPlugin] artifact auto-reload failed", { error: e?.message });
3154
+ } finally {
3155
+ pending = false;
3156
+ }
3157
+ };
3158
+ w.on("change", () => {
3159
+ void reload();
3160
+ });
3161
+ w.on("add", () => {
3162
+ void reload();
3163
+ });
3164
+ this.artifactWatcher = { close: () => w.close() };
3165
+ console.log("[MetadataPlugin] artifact file watcher attached", src2.path);
3166
+ } catch (e) {
3167
+ ctx.logger.warn("[MetadataPlugin] artifact watcher failed to start", { error: e?.message });
3168
+ }
3169
+ }
2982
3170
  console.log("[MetadataPlugin] HMR endpoint registered at /api/v1/dev/metadata-events");
2983
3171
  } else {
2984
3172
  console.log("[MetadataPlugin] HTTP server with getRawApp() not available \u2014 skipping HMR endpoint");
@@ -2987,6 +3175,28 @@ var MetadataPlugin = class {
2987
3175
  console.warn("[MetadataPlugin] Failed to register HMR endpoint", e?.message);
2988
3176
  }
2989
3177
  };
3178
+ this.stop = async (ctx) => {
3179
+ if (this.artifactWatcher) {
3180
+ try {
3181
+ await this.artifactWatcher.close();
3182
+ } catch {
3183
+ }
3184
+ this.artifactWatcher = void 0;
3185
+ }
3186
+ try {
3187
+ await this.manager.dispose();
3188
+ } catch (e) {
3189
+ ctx.logger.warn("[MetadataPlugin] manager.dispose() failed", { error: e?.message });
3190
+ }
3191
+ const repo = this.repository;
3192
+ if (repo && typeof repo.close === "function") {
3193
+ try {
3194
+ await repo.close();
3195
+ } catch {
3196
+ }
3197
+ }
3198
+ this.repository = void 0;
3199
+ };
2990
3200
  this.options = {
2991
3201
  watch: true,
2992
3202
  ...options
@@ -3063,7 +3273,12 @@ var MetadataPlugin = class {
3063
3273
  const items = metadata[field];
3064
3274
  if (!Array.isArray(items) || items.length === 0) continue;
3065
3275
  for (const item of items) {
3066
- const name = item?.name;
3276
+ let name = item?.name;
3277
+ if (!name) {
3278
+ if (metaType === "view") {
3279
+ name = item?.list?.data?.object ?? item?.form?.data?.object;
3280
+ }
3281
+ }
3067
3282
  if (!name) continue;
3068
3283
  if (manifestPackageId && item._packageId === void 0) {
3069
3284
  item._packageId = manifestPackageId;
@@ -3372,6 +3587,10 @@ function registerMetadataHistoryRoutes(app, metadataService) {
3372
3587
  }
3373
3588
 
3374
3589
  // src/utils/history-cleanup.ts
3590
+ import { DEFAULT_METADATA_TYPE_REGISTRY as DEFAULT_METADATA_TYPE_REGISTRY2 } from "@objectstack/spec/kernel";
3591
+ function executionPinnedTypes() {
3592
+ return DEFAULT_METADATA_TYPE_REGISTRY2.filter((entry) => entry.executionPinned).map((entry) => entry.type);
3593
+ }
3375
3594
  var HistoryCleanupManager = class {
3376
3595
  constructor(policy, dbLoader) {
3377
3596
  this.policy = policy;
@@ -3407,9 +3626,10 @@ var HistoryCleanupManager = class {
3407
3626
  const driver = this.dbLoader.driver;
3408
3627
  const historyTableName = this.dbLoader.historyTableName;
3409
3628
  const organizationId = this.dbLoader.organizationId;
3410
- const projectId = this.dbLoader.projectId;
3411
3629
  let deleted = 0;
3412
3630
  let errors = 0;
3631
+ const pinnedTypes = executionPinnedTypes();
3632
+ const isPinned = (t) => !!t && pinnedTypes.includes(t);
3413
3633
  try {
3414
3634
  if (this.policy.maxAgeDays) {
3415
3635
  const cutoffDate = /* @__PURE__ */ new Date();
@@ -3421,8 +3641,8 @@ var HistoryCleanupManager = class {
3421
3641
  if (organizationId) {
3422
3642
  filter.organization_id = organizationId;
3423
3643
  }
3424
- if (projectId !== void 0) {
3425
- filter.project_id = projectId;
3644
+ if (pinnedTypes.length > 0) {
3645
+ filter.type = { $nin: pinnedTypes };
3426
3646
  }
3427
3647
  try {
3428
3648
  const result = await this.bulkDeleteByFilter(driver, historyTableName, filter);
@@ -3436,20 +3656,22 @@ var HistoryCleanupManager = class {
3436
3656
  try {
3437
3657
  const baseWhere = {};
3438
3658
  if (organizationId) baseWhere.organization_id = organizationId;
3439
- if (projectId !== void 0) baseWhere.project_id = projectId;
3440
- const metadataIds = await driver.find(historyTableName, {
3659
+ const metaItems = await driver.find(historyTableName, {
3441
3660
  object: historyTableName,
3442
3661
  where: baseWhere,
3443
- fields: ["metadata_id"]
3662
+ fields: ["type", "name"]
3444
3663
  });
3445
- const uniqueIds = /* @__PURE__ */ new Set();
3446
- for (const record of metadataIds) {
3447
- if (record.metadata_id) {
3448
- uniqueIds.add(record.metadata_id);
3664
+ const uniqueKeys = /* @__PURE__ */ new Set();
3665
+ for (const record of metaItems) {
3666
+ const t = record.type;
3667
+ const n = record.name;
3668
+ if (t && n && !isPinned(t)) {
3669
+ uniqueKeys.add(`${t}${n}`);
3449
3670
  }
3450
3671
  }
3451
- for (const metadataId of uniqueIds) {
3452
- const filter = { metadata_id: metadataId, ...baseWhere };
3672
+ for (const key of uniqueKeys) {
3673
+ const [type, name] = key.split("");
3674
+ const filter = { type, name, ...baseWhere };
3453
3675
  try {
3454
3676
  const historyRecords = await driver.find(historyTableName, {
3455
3677
  object: historyTableName,
@@ -3524,13 +3746,13 @@ var HistoryCleanupManager = class {
3524
3746
  const driver = this.dbLoader.driver;
3525
3747
  const historyTableName = this.dbLoader.historyTableName;
3526
3748
  const organizationId = this.dbLoader.organizationId;
3527
- const projectId = this.dbLoader.projectId;
3528
3749
  let recordsByAge = 0;
3529
3750
  let recordsByCount = 0;
3751
+ const pinnedTypes = executionPinnedTypes();
3752
+ const isPinned = (t) => !!t && pinnedTypes.includes(t);
3530
3753
  try {
3531
3754
  const baseWhere = {};
3532
3755
  if (organizationId) baseWhere.organization_id = organizationId;
3533
- if (projectId !== void 0) baseWhere.project_id = projectId;
3534
3756
  if (this.policy.maxAgeDays) {
3535
3757
  const cutoffDate = /* @__PURE__ */ new Date();
3536
3758
  cutoffDate.setDate(cutoffDate.getDate() - this.policy.maxAgeDays);
@@ -3539,25 +3761,31 @@ var HistoryCleanupManager = class {
3539
3761
  recorded_at: { $lt: cutoffISO },
3540
3762
  ...baseWhere
3541
3763
  };
3764
+ if (pinnedTypes.length > 0) {
3765
+ filter.type = { $nin: pinnedTypes };
3766
+ }
3542
3767
  recordsByAge = await driver.count(historyTableName, {
3543
3768
  object: historyTableName,
3544
3769
  where: filter
3545
3770
  });
3546
3771
  }
3547
3772
  if (this.policy.maxVersions) {
3548
- const metadataIds = await driver.find(historyTableName, {
3773
+ const metaItems = await driver.find(historyTableName, {
3549
3774
  object: historyTableName,
3550
3775
  where: baseWhere,
3551
- fields: ["metadata_id"]
3776
+ fields: ["type", "name"]
3552
3777
  });
3553
- const uniqueIds = /* @__PURE__ */ new Set();
3554
- for (const record of metadataIds) {
3555
- if (record.metadata_id) {
3556
- uniqueIds.add(record.metadata_id);
3778
+ const uniqueKeys = /* @__PURE__ */ new Set();
3779
+ for (const record of metaItems) {
3780
+ const t = record.type;
3781
+ const n = record.name;
3782
+ if (t && n && !isPinned(t)) {
3783
+ uniqueKeys.add(`${t}${n}`);
3557
3784
  }
3558
3785
  }
3559
- for (const metadataId of uniqueIds) {
3560
- const filter = { metadata_id: metadataId, ...baseWhere };
3786
+ for (const key of uniqueKeys) {
3787
+ const [type, name] = key.split("");
3788
+ const filter = { type, name, ...baseWhere };
3561
3789
  const count = await driver.count(historyTableName, {
3562
3790
  object: historyTableName,
3563
3791
  where: filter