@objectstack/metadata 4.1.1 → 5.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/index.cjs CHANGED
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
8
11
  var __export = (target, all) => {
9
12
  for (var name in all)
10
13
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -27,6 +30,160 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
30
  ));
28
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
32
 
33
+ // src/routes/hmr-routes.ts
34
+ var hmr_routes_exports = {};
35
+ __export(hmr_routes_exports, {
36
+ registerMetadataHmrRoutes: () => registerMetadataHmrRoutes
37
+ });
38
+ function registerMetadataHmrRoutes(app, manager, options = {}) {
39
+ const routePath = options.path ?? "/api/v1/dev/metadata-events";
40
+ const listeners = /* @__PURE__ */ new Set();
41
+ const broadcast = (evt) => {
42
+ for (const l of listeners) {
43
+ try {
44
+ l(evt);
45
+ } catch {
46
+ }
47
+ }
48
+ };
49
+ let fsHookInstalled = false;
50
+ const installFsHooks = async () => {
51
+ if (fsHookInstalled) return;
52
+ const mgr = manager;
53
+ if (typeof mgr.subscribe !== "function") {
54
+ fsHookInstalled = true;
55
+ return;
56
+ }
57
+ const types = await manager.getRegisteredTypes();
58
+ for (const type of types) {
59
+ mgr.subscribe(type, (evt) => {
60
+ const ts = typeof evt.timestamp === "string" ? Date.parse(evt.timestamp) : evt.timestamp ?? Date.now();
61
+ broadcast({
62
+ kind: "metadata-change",
63
+ type: evt.type ?? "changed",
64
+ metadataType: evt.metadataType ?? type,
65
+ name: evt.name ?? "",
66
+ path: evt.path,
67
+ timestamp: Number.isFinite(ts) ? ts : Date.now(),
68
+ // Forward the canonical server-side sequence number when the
69
+ // event originated from a MetadataRepository (ADR-0008). Legacy
70
+ // chokidar-driven events have no seq — clients fall back to
71
+ // their local counter in that case.
72
+ ...typeof evt.seq === "number" ? { seq: evt.seq } : {}
73
+ });
74
+ });
75
+ }
76
+ fsHookInstalled = true;
77
+ };
78
+ installFsHooks().catch(() => {
79
+ });
80
+ let onPostReload = null;
81
+ app.get(routePath, async (c) => {
82
+ await installFsHooks().catch(() => {
83
+ });
84
+ const types = await manager.getRegisteredTypes().catch(() => []);
85
+ const stream = new ReadableStream({
86
+ async start(controller) {
87
+ const enc = new TextEncoder();
88
+ let closed = false;
89
+ const safeEnqueue = (chunk) => {
90
+ if (closed) return;
91
+ try {
92
+ controller.enqueue(enc.encode(chunk));
93
+ } catch {
94
+ closed = true;
95
+ }
96
+ };
97
+ const listener = (evt) => {
98
+ if (closed) return;
99
+ const eventName = evt.kind === "reload" ? "reload" : "metadata-change";
100
+ safeEnqueue(`event: ${eventName}
101
+ data: ${JSON.stringify(evt)}
102
+
103
+ `);
104
+ };
105
+ listeners.add(listener);
106
+ safeEnqueue(`event: ready
107
+ data: ${JSON.stringify({ types, timestamp: Date.now() })}
108
+
109
+ `);
110
+ const heartbeat = setInterval(() => {
111
+ safeEnqueue(`: ping ${Date.now()}
112
+
113
+ `);
114
+ }, 15e3);
115
+ const cleanup = () => {
116
+ if (closed) return;
117
+ closed = true;
118
+ clearInterval(heartbeat);
119
+ listeners.delete(listener);
120
+ try {
121
+ controller.close();
122
+ } catch {
123
+ }
124
+ };
125
+ const signal = c.req?.raw?.signal;
126
+ if (signal) {
127
+ if (signal.aborted) cleanup();
128
+ else signal.addEventListener("abort", cleanup, { once: true });
129
+ }
130
+ }
131
+ });
132
+ return new Response(stream, {
133
+ status: 200,
134
+ headers: {
135
+ "Content-Type": "text/event-stream; charset=utf-8",
136
+ "Cache-Control": "no-cache, no-transform",
137
+ "Connection": "keep-alive",
138
+ "X-Accel-Buffering": "no"
139
+ }
140
+ });
141
+ });
142
+ app.post(routePath, async (c) => {
143
+ let body = {};
144
+ try {
145
+ const ct = c.req?.header?.("content-type") ?? "";
146
+ if (typeof c.req?.json === "function" && ct.includes("json")) {
147
+ body = await c.req.json();
148
+ }
149
+ } catch {
150
+ }
151
+ try {
152
+ if (onPostReload) await onPostReload(body);
153
+ } catch (e) {
154
+ return new Response(
155
+ JSON.stringify({ ok: false, error: e?.message ?? "reload handler failed" }),
156
+ { status: 500, headers: { "Content-Type": "application/json" } }
157
+ );
158
+ }
159
+ const reason = body.reason ?? "manual-trigger";
160
+ broadcast({
161
+ kind: "reload",
162
+ reason,
163
+ changed: body.changed,
164
+ timestamp: Date.now()
165
+ });
166
+ return new Response(
167
+ JSON.stringify({ ok: true, listeners: listeners.size, reason }),
168
+ { status: 200, headers: { "Content-Type": "application/json" } }
169
+ );
170
+ });
171
+ return {
172
+ broadcastReload(reason, changed) {
173
+ broadcast({ kind: "reload", reason, changed, timestamp: Date.now() });
174
+ },
175
+ setOnPostReload(fn) {
176
+ onPostReload = fn;
177
+ },
178
+ listenerCount: () => listeners.size
179
+ };
180
+ }
181
+ var init_hmr_routes = __esm({
182
+ "src/routes/hmr-routes.ts"() {
183
+ "use strict";
184
+ }
185
+ });
186
+
30
187
  // src/index.ts
31
188
  var index_exports = {};
32
189
  __export(index_exports, {
@@ -451,7 +608,7 @@ var DatabaseLoader = class {
451
608
  this.tableName = options.tableName ?? "sys_metadata";
452
609
  this.historyTableName = options.historyTableName ?? "sys_metadata_history";
453
610
  this.organizationId = options.organizationId;
454
- this.projectId = options.projectId;
611
+ void options.projectId;
455
612
  this.trackHistory = options.trackHistory !== false;
456
613
  const cacheOpts = options.cache;
457
614
  const cacheEnabled = cacheOpts?.enabled !== false;
@@ -541,6 +698,26 @@ var DatabaseLoader = class {
541
698
  }
542
699
  return this.driver.delete(table, id);
543
700
  }
701
+ /**
702
+ * Compute the next per-org `event_seq` for `sys_metadata_history`.
703
+ * Reads `MAX(event_seq) + 1` for the configured `organization_id`.
704
+ * Legacy path — not transactional, so concurrent writes can collide.
705
+ * The canonical (transactional) producer is `SysMetadataRepository`.
706
+ */
707
+ async nextEventSeq() {
708
+ const where = this.organizationId ? { organization_id: this.organizationId } : {};
709
+ try {
710
+ const rows = await this._find(this.historyTableName, { where });
711
+ let max = 0;
712
+ for (const row of rows) {
713
+ const v = typeof row.event_seq === "number" ? row.event_seq : 0;
714
+ if (v > max) max = v;
715
+ }
716
+ return max + 1;
717
+ } catch {
718
+ return 1;
719
+ }
720
+ }
544
721
  /**
545
722
  * Ensure the metadata table exists.
546
723
  * Uses IDataDriver.syncSchema with the SysMetadataObject definition
@@ -605,8 +782,9 @@ var DatabaseLoader = class {
605
782
  }
606
783
  /**
607
784
  * Build base filter conditions for queries.
608
- * Filters by organizationId when configured; project_id when projectId is set,
609
- * or null (platform-global) when not set.
785
+ * Filters by organizationId when configured. `projectId` is accepted
786
+ * for back-compat but no longer constrains the query — see
787
+ * ADR-0008 §0 (branch/project removal).
610
788
  */
611
789
  baseFilter(type, name) {
612
790
  const filter = { type };
@@ -616,7 +794,6 @@ var DatabaseLoader = class {
616
794
  if (this.organizationId) {
617
795
  filter.organization_id = this.organizationId;
618
796
  }
619
- filter.project_id = this.projectId ?? null;
620
797
  return filter;
621
798
  }
622
799
  /**
@@ -642,6 +819,7 @@ var DatabaseLoader = class {
642
819
  }
643
820
  const historyId = generateId();
644
821
  const metadataJson = JSON.stringify(metadata);
822
+ const eventSeq = await this.nextEventSeq();
645
823
  const historyRecord = {
646
824
  id: historyId,
647
825
  metadataId,
@@ -655,12 +833,12 @@ var DatabaseLoader = class {
655
833
  changeNote,
656
834
  recordedBy,
657
835
  recordedAt: now,
658
- ...this.organizationId ? { organizationId: this.organizationId } : {},
659
- ...this.projectId !== void 0 ? { projectId: this.projectId } : {}
836
+ ...this.organizationId ? { organizationId: this.organizationId } : {}
660
837
  };
661
838
  try {
662
839
  await this._create(this.historyTableName, {
663
840
  id: historyRecord.id,
841
+ event_seq: eventSeq,
664
842
  metadata_id: historyRecord.metadataId,
665
843
  name: historyRecord.name,
666
844
  type: historyRecord.type,
@@ -672,8 +850,8 @@ var DatabaseLoader = class {
672
850
  change_note: historyRecord.changeNote,
673
851
  recorded_by: historyRecord.recordedBy,
674
852
  recorded_at: historyRecord.recordedAt,
675
- ...this.organizationId ? { organization_id: this.organizationId } : {},
676
- ...this.projectId !== void 0 ? { project_id: this.projectId } : {}
853
+ source: "database-loader",
854
+ ...this.organizationId ? { organization_id: this.organizationId } : {}
677
855
  });
678
856
  } catch (error) {
679
857
  console.error(`Failed to create history record for ${type}/${name}:`, error);
@@ -860,7 +1038,6 @@ var DatabaseLoader = class {
860
1038
  if (this.organizationId) {
861
1039
  filter.organization_id = this.organizationId;
862
1040
  }
863
- filter.project_id = this.projectId ?? null;
864
1041
  const row = await this._findOne(this.historyTableName, {
865
1042
  where: filter
866
1043
  });
@@ -877,7 +1054,6 @@ var DatabaseLoader = class {
877
1054
  previousChecksum: row.previous_checksum,
878
1055
  changeNote: row.change_note,
879
1056
  organizationId: row.organization_id,
880
- projectId: row.project_id,
881
1057
  recordedBy: row.recorded_by,
882
1058
  recordedAt: row.recorded_at
883
1059
  };
@@ -895,7 +1071,6 @@ var DatabaseLoader = class {
895
1071
  await this.ensureHistorySchema();
896
1072
  const filter = { type, name };
897
1073
  if (this.organizationId) filter.organization_id = this.organizationId;
898
- filter.project_id = this.projectId ?? null;
899
1074
  const metadataRecord = await this._findOne(this.tableName, { where: filter });
900
1075
  if (!metadataRecord) {
901
1076
  return { records: [], total: 0, hasMore: false };
@@ -904,7 +1079,6 @@ var DatabaseLoader = class {
904
1079
  metadata_id: metadataRecord.id
905
1080
  };
906
1081
  if (this.organizationId) historyFilter.organization_id = this.organizationId;
907
- historyFilter.project_id = this.projectId ?? null;
908
1082
  if (options?.operationType) historyFilter.operation_type = options.operationType;
909
1083
  if (options?.since) historyFilter.recorded_at = { $gte: options.since };
910
1084
  if (options?.until) {
@@ -943,7 +1117,6 @@ var DatabaseLoader = class {
943
1117
  previousChecksum: row.previous_checksum,
944
1118
  changeNote: row.change_note,
945
1119
  organizationId: row.organization_id,
946
- projectId: row.project_id,
947
1120
  recordedBy: row.recorded_by,
948
1121
  recordedAt: row.recorded_at
949
1122
  };
@@ -1050,7 +1223,6 @@ var DatabaseLoader = class {
1050
1223
  version: 1,
1051
1224
  source: "database",
1052
1225
  ...this.organizationId ? { organization_id: this.organizationId } : {},
1053
- ...this.projectId !== void 0 ? { project_id: this.projectId } : { project_id: null },
1054
1226
  created_at: now,
1055
1227
  updated_at: now
1056
1228
  });
@@ -1123,6 +1295,7 @@ var _MetadataManager = class _MetadataManager {
1123
1295
  // Invalidated on every `register()` / `unregister()` to keep CRUD writes
1124
1296
  // visible to subsequent reads.
1125
1297
  this.listCache = /* @__PURE__ */ new Map();
1298
+ this.repoWatchClosed = false;
1126
1299
  this.config = config;
1127
1300
  this.logger = (0, import_core.createLogger)({ level: "info", format: "pretty" });
1128
1301
  this.serializers = /* @__PURE__ */ new Map();
@@ -1818,6 +1991,21 @@ var _MetadataManager = class _MetadataManager {
1818
1991
  unsubscribe: () => this.removeWatchCallback(type, wrappedCallback)
1819
1992
  };
1820
1993
  }
1994
+ /**
1995
+ * Subscribe to raw metadata watch events for a given type.
1996
+ *
1997
+ * Unlike `watchService` (which maps to the IMetadataService contract and
1998
+ * drops fields like `path`/`timestamp`), this returns the raw
1999
+ * `MetadataWatchEvent` produced by the underlying watcher — useful for
2000
+ * developer-facing tooling such as the HMR SSE endpoint that wants the
2001
+ * source file path and original timestamp.
2002
+ *
2003
+ * @returns An unsubscribe function.
2004
+ */
2005
+ subscribe(type, callback) {
2006
+ this.addWatchCallback(type, callback);
2007
+ return () => this.removeWatchCallback(type, callback);
2008
+ }
1821
2009
  // ==========================================
1822
2010
  // Import / Export
1823
2011
  // ==========================================
@@ -2115,6 +2303,110 @@ var _MetadataManager = class _MetadataManager {
2115
2303
  */
2116
2304
  async stopWatching() {
2117
2305
  }
2306
+ // ─── ADR-0008 PR-6: Repository wiring ───────────────────────────────
2307
+ /**
2308
+ * Attach a {@link MetadataRepository} as a supplementary event source.
2309
+ *
2310
+ * The manager subscribes to `repo.watch({})` and re-emits each event
2311
+ * through {@link notifyWatchers} as a legacy `MetadataWatchEvent`.
2312
+ * Each event also invalidates the in-memory registry entry and the
2313
+ * `list()` cache for the affected type so subsequent reads see fresh
2314
+ * data.
2315
+ *
2316
+ * No write-through. `register()` / `unregister()` / `save()` are
2317
+ * untouched in this PR (deferred to ADR-0008 M0 PR-10).
2318
+ *
2319
+ * Call {@link dispose} (or {@link stopRepositoryWatch}) to detach.
2320
+ */
2321
+ setRepository(repo) {
2322
+ if (this.repository === repo) return;
2323
+ if (this.repository) {
2324
+ void this.stopRepositoryWatch();
2325
+ }
2326
+ this.repository = repo;
2327
+ this.repoWatchClosed = false;
2328
+ void this.startRepositoryWatch();
2329
+ }
2330
+ /** Return the attached repository, if any. */
2331
+ getRepository() {
2332
+ return this.repository;
2333
+ }
2334
+ /** Stop the active repo.watch() loop (best-effort). */
2335
+ async stopRepositoryWatch() {
2336
+ this.repoWatchClosed = true;
2337
+ const iter = this.repoWatchIter;
2338
+ this.repoWatchIter = void 0;
2339
+ if (iter && typeof iter.return === "function") {
2340
+ try {
2341
+ await iter.return(void 0);
2342
+ } catch {
2343
+ }
2344
+ }
2345
+ }
2346
+ /**
2347
+ * Best-effort cleanup. Stops the FS watcher (if any), drains the
2348
+ * repository watch loop, and clears registry caches. Safe to call
2349
+ * multiple times.
2350
+ */
2351
+ async dispose() {
2352
+ await this.stopWatching().catch(() => void 0);
2353
+ await this.stopRepositoryWatch().catch(() => void 0);
2354
+ this.listCache.clear();
2355
+ }
2356
+ async startRepositoryWatch() {
2357
+ const repo = this.repository;
2358
+ if (!repo) return;
2359
+ const iterable = repo.watch({});
2360
+ const iter = iterable[Symbol.asyncIterator]();
2361
+ this.repoWatchIter = iter;
2362
+ try {
2363
+ while (!this.repoWatchClosed) {
2364
+ const { value, done } = await iter.next();
2365
+ if (done) break;
2366
+ try {
2367
+ this.applyRepoEvent(value);
2368
+ } catch (err) {
2369
+ this.logger.warn("[MetadataManager] repo event handler failed", {
2370
+ error: err instanceof Error ? err.message : String(err)
2371
+ });
2372
+ }
2373
+ }
2374
+ } catch (err) {
2375
+ if (!this.repoWatchClosed) {
2376
+ this.logger.warn("[MetadataManager] repository watch loop exited unexpectedly", {
2377
+ error: err instanceof Error ? err.message : String(err)
2378
+ });
2379
+ }
2380
+ } finally {
2381
+ if (this.repoWatchIter === iter) this.repoWatchIter = void 0;
2382
+ }
2383
+ }
2384
+ /** Translate a repo event to the legacy MetadataWatchEvent + invalidate caches. */
2385
+ applyRepoEvent(evt) {
2386
+ const ref = evt.ref;
2387
+ const type = ref.type;
2388
+ const name = ref.name;
2389
+ const typeStore = this.registry.get(type);
2390
+ if (typeStore) {
2391
+ typeStore.delete(name);
2392
+ if (typeStore.size === 0) this.registry.delete(type);
2393
+ }
2394
+ this.listCache.delete(type);
2395
+ const legacyType = evt.op === "create" ? "added" : evt.op === "delete" ? "deleted" : "changed";
2396
+ const legacyEvent = {
2397
+ type: legacyType,
2398
+ metadataType: type,
2399
+ name,
2400
+ path: "",
2401
+ // Repo events carry the hash only; the body is fetched on demand
2402
+ // via manager.get(type, name). HMR consumers don't read `data` so
2403
+ // this is fine for M0. (See ADR-0008 §12 open question 1.)
2404
+ data: void 0,
2405
+ timestamp: evt.ts
2406
+ };
2407
+ legacyEvent.seq = evt.seq;
2408
+ this.notifyWatchers(type, legacyEvent);
2409
+ }
2118
2410
  notifyWatchers(type, event) {
2119
2411
  const callbacks = this.watchCallbacks.get(type);
2120
2412
  if (!callbacks) return;
@@ -2590,7 +2882,13 @@ var NodeMetadataManager = class extends MetadataManager {
2590
2882
  this.watcher = (0, import_chokidar.watch)(rootDir, {
2591
2883
  ignored,
2592
2884
  persistent,
2593
- ignoreInitial: true
2885
+ ignoreInitial: true,
2886
+ // Use polling to avoid `fs.watch` EMFILE on macOS / busy dev hosts.
2887
+ // Recursive watch over a project root would otherwise wire native
2888
+ // watches across the entire tree, easily exhausting the FD pool.
2889
+ usePolling: true,
2890
+ interval: 1e3,
2891
+ binaryInterval: 2e3
2594
2892
  });
2595
2893
  this.watcher.on("add", async (filePath) => {
2596
2894
  await this.handleFileEvent("added", filePath);
@@ -2725,6 +3023,7 @@ var queryableMetadataObjects = [
2725
3023
  import_metadata2.SysMetadataObject,
2726
3024
  import_metadata2.SysMetadataHistoryObject
2727
3025
  ];
3026
+ var REPO_SUBDIR = ".objectstack/metadata";
2728
3027
  var ARTIFACT_FIELD_TO_TYPE = {
2729
3028
  objects: "object",
2730
3029
  objectExtensions: "object_extension",
@@ -2822,6 +3121,31 @@ var MetadataPlugin = class {
2822
3121
  await this._loadFromFileSystem(ctx);
2823
3122
  }
2824
3123
  }
3124
+ const bootstrapMode = this.options.config?.bootstrap ?? "eager";
3125
+ if (bootstrapMode !== "artifact-only") {
3126
+ try {
3127
+ const path3 = await import("path");
3128
+ const { FileSystemRepository } = await import("@objectstack/metadata-fs");
3129
+ const rootDir = this.options.rootDir || process.cwd();
3130
+ const repoRoot = path3.join(rootDir, REPO_SUBDIR);
3131
+ const repo = new FileSystemRepository({
3132
+ root: repoRoot,
3133
+ org: this.options.organizationId ?? "system",
3134
+ disableWatch: this.options.watch === false
3135
+ });
3136
+ await repo.start();
3137
+ this.repository = repo;
3138
+ this.manager.setRepository(repo);
3139
+ ctx.logger.info("[MetadataPlugin] FileSystemRepository attached", {
3140
+ repoRoot,
3141
+ watch: this.options.watch !== false
3142
+ });
3143
+ } catch (e) {
3144
+ ctx.logger.warn("[MetadataPlugin] Failed to attach FileSystemRepository", {
3145
+ error: e?.message
3146
+ });
3147
+ }
3148
+ }
2825
3149
  try {
2826
3150
  const realtimeService = ctx.getService("realtime");
2827
3151
  if (realtimeService && typeof realtimeService === "object" && "publish" in realtimeService) {
@@ -2833,6 +3157,102 @@ var MetadataPlugin = class {
2833
3157
  error: e.message
2834
3158
  });
2835
3159
  }
3160
+ try {
3161
+ const httpServer = ctx.getService("http-server") ?? ctx.getService("http.server");
3162
+ if (httpServer && typeof httpServer.getRawApp === "function") {
3163
+ const { registerMetadataHmrRoutes: registerMetadataHmrRoutes2 } = await Promise.resolve().then(() => (init_hmr_routes(), hmr_routes_exports));
3164
+ const hub = registerMetadataHmrRoutes2(httpServer.getRawApp(), this.manager);
3165
+ hub.setOnPostReload(async (body = {}) => {
3166
+ const src3 = this.options.artifactSource;
3167
+ if (src3?.mode === "local-file") {
3168
+ try {
3169
+ await this._loadFromLocalFile(ctx, src3.path, src3.fetchTimeoutMs);
3170
+ ctx.logger.info("[MetadataPlugin] artifact reloaded via HMR POST", {
3171
+ path: src3.path,
3172
+ reason: body?.reason
3173
+ });
3174
+ } catch (e) {
3175
+ ctx.logger.warn("[MetadataPlugin] artifact reload failed", { error: e?.message });
3176
+ throw e;
3177
+ }
3178
+ }
3179
+ });
3180
+ const src2 = this.options.artifactSource;
3181
+ const wantArtifactWatch = this.options.artifactWatch ?? src2?.mode === "local-file";
3182
+ if (src2?.mode === "local-file" && wantArtifactWatch && !/^https?:\/\//i.test(src2.path)) {
3183
+ try {
3184
+ const { watch: chokidarWatch2 } = await import("chokidar");
3185
+ const w = chokidarWatch2(src2.path, {
3186
+ ignoreInitial: true,
3187
+ awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 20 },
3188
+ persistent: true,
3189
+ // Use polling to avoid `fs.watch` exhausting the
3190
+ // process file-descriptor limit on macOS (chokidar
3191
+ // recursively wires watches on the parent
3192
+ // directory tree which can trip EMFILE on busy
3193
+ // dev hosts). 500ms polling is fast enough for
3194
+ // HMR (a recompile takes ~400ms anyway).
3195
+ usePolling: true,
3196
+ interval: 500,
3197
+ binaryInterval: 1e3
3198
+ });
3199
+ let pending = false;
3200
+ const reload = async () => {
3201
+ if (pending) return;
3202
+ pending = true;
3203
+ try {
3204
+ await this._loadFromLocalFile(ctx, src2.path, src2.fetchTimeoutMs);
3205
+ hub.broadcastReload("artifact-file-changed", [src2.path]);
3206
+ ctx.logger.info("[MetadataPlugin] artifact auto-reloaded (file watcher)", {
3207
+ path: src2.path
3208
+ });
3209
+ } catch (e) {
3210
+ ctx.logger.warn("[MetadataPlugin] artifact auto-reload failed", { error: e?.message });
3211
+ } finally {
3212
+ pending = false;
3213
+ }
3214
+ };
3215
+ w.on("change", () => {
3216
+ void reload();
3217
+ });
3218
+ w.on("add", () => {
3219
+ void reload();
3220
+ });
3221
+ this.artifactWatcher = { close: () => w.close() };
3222
+ console.log("[MetadataPlugin] artifact file watcher attached", src2.path);
3223
+ } catch (e) {
3224
+ ctx.logger.warn("[MetadataPlugin] artifact watcher failed to start", { error: e?.message });
3225
+ }
3226
+ }
3227
+ console.log("[MetadataPlugin] HMR endpoint registered at /api/v1/dev/metadata-events");
3228
+ } else {
3229
+ console.log("[MetadataPlugin] HTTP server with getRawApp() not available \u2014 skipping HMR endpoint");
3230
+ }
3231
+ } catch (e) {
3232
+ console.warn("[MetadataPlugin] Failed to register HMR endpoint", e?.message);
3233
+ }
3234
+ };
3235
+ this.stop = async (ctx) => {
3236
+ if (this.artifactWatcher) {
3237
+ try {
3238
+ await this.artifactWatcher.close();
3239
+ } catch {
3240
+ }
3241
+ this.artifactWatcher = void 0;
3242
+ }
3243
+ try {
3244
+ await this.manager.dispose();
3245
+ } catch (e) {
3246
+ ctx.logger.warn("[MetadataPlugin] manager.dispose() failed", { error: e?.message });
3247
+ }
3248
+ const repo = this.repository;
3249
+ if (repo && typeof repo.close === "function") {
3250
+ try {
3251
+ await repo.close();
3252
+ } catch {
3253
+ }
3254
+ }
3255
+ this.repository = void 0;
2836
3256
  };
2837
3257
  this.options = {
2838
3258
  watch: true,
@@ -2910,7 +3330,12 @@ var MetadataPlugin = class {
2910
3330
  const items = metadata[field];
2911
3331
  if (!Array.isArray(items) || items.length === 0) continue;
2912
3332
  for (const item of items) {
2913
- const name = item?.name;
3333
+ let name = item?.name;
3334
+ if (!name) {
3335
+ if (metaType === "view") {
3336
+ name = item?.list?.data?.object ?? item?.form?.data?.object;
3337
+ }
3338
+ }
2914
3339
  if (!name) continue;
2915
3340
  if (manifestPackageId && item._packageId === void 0) {
2916
3341
  item._packageId = manifestPackageId;
@@ -3254,7 +3679,6 @@ var HistoryCleanupManager = class {
3254
3679
  const driver = this.dbLoader.driver;
3255
3680
  const historyTableName = this.dbLoader.historyTableName;
3256
3681
  const organizationId = this.dbLoader.organizationId;
3257
- const projectId = this.dbLoader.projectId;
3258
3682
  let deleted = 0;
3259
3683
  let errors = 0;
3260
3684
  try {
@@ -3268,9 +3692,6 @@ var HistoryCleanupManager = class {
3268
3692
  if (organizationId) {
3269
3693
  filter.organization_id = organizationId;
3270
3694
  }
3271
- if (projectId !== void 0) {
3272
- filter.project_id = projectId;
3273
- }
3274
3695
  try {
3275
3696
  const result = await this.bulkDeleteByFilter(driver, historyTableName, filter);
3276
3697
  deleted += result.deleted;
@@ -3283,7 +3704,6 @@ var HistoryCleanupManager = class {
3283
3704
  try {
3284
3705
  const baseWhere = {};
3285
3706
  if (organizationId) baseWhere.organization_id = organizationId;
3286
- if (projectId !== void 0) baseWhere.project_id = projectId;
3287
3707
  const metadataIds = await driver.find(historyTableName, {
3288
3708
  object: historyTableName,
3289
3709
  where: baseWhere,
@@ -3371,13 +3791,11 @@ var HistoryCleanupManager = class {
3371
3791
  const driver = this.dbLoader.driver;
3372
3792
  const historyTableName = this.dbLoader.historyTableName;
3373
3793
  const organizationId = this.dbLoader.organizationId;
3374
- const projectId = this.dbLoader.projectId;
3375
3794
  let recordsByAge = 0;
3376
3795
  let recordsByCount = 0;
3377
3796
  try {
3378
3797
  const baseWhere = {};
3379
3798
  if (organizationId) baseWhere.organization_id = organizationId;
3380
- if (projectId !== void 0) baseWhere.project_id = projectId;
3381
3799
  if (this.policy.maxAgeDays) {
3382
3800
  const cutoffDate = /* @__PURE__ */ new Date();
3383
3801
  cutoffDate.setDate(cutoffDate.getDate() - this.policy.maxAgeDays);