@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 +441 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +89 -4
- package/dist/index.d.ts +89 -4
- package/dist/index.js +442 -23
- package/dist/index.js.map +1 -1
- package/dist/node.cjs +441 -23
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +1 -0
- package/dist/node.d.ts +1 -0
- package/dist/node.js +442 -23
- package/dist/node.js.map +1 -1
- package/package.json +7 -5
package/dist/index.js
CHANGED
|
@@ -1,9 +1,167 @@
|
|
|
1
1
|
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
2
6
|
var __export = (target, all) => {
|
|
3
7
|
for (var name in all)
|
|
4
8
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
9
|
};
|
|
6
10
|
|
|
11
|
+
// src/routes/hmr-routes.ts
|
|
12
|
+
var hmr_routes_exports = {};
|
|
13
|
+
__export(hmr_routes_exports, {
|
|
14
|
+
registerMetadataHmrRoutes: () => registerMetadataHmrRoutes
|
|
15
|
+
});
|
|
16
|
+
function registerMetadataHmrRoutes(app, manager, options = {}) {
|
|
17
|
+
const routePath = options.path ?? "/api/v1/dev/metadata-events";
|
|
18
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
19
|
+
const broadcast = (evt) => {
|
|
20
|
+
for (const l of listeners) {
|
|
21
|
+
try {
|
|
22
|
+
l(evt);
|
|
23
|
+
} catch {
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
let fsHookInstalled = false;
|
|
28
|
+
const installFsHooks = async () => {
|
|
29
|
+
if (fsHookInstalled) return;
|
|
30
|
+
const mgr = manager;
|
|
31
|
+
if (typeof mgr.subscribe !== "function") {
|
|
32
|
+
fsHookInstalled = true;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const types = await manager.getRegisteredTypes();
|
|
36
|
+
for (const type of types) {
|
|
37
|
+
mgr.subscribe(type, (evt) => {
|
|
38
|
+
const ts = typeof evt.timestamp === "string" ? Date.parse(evt.timestamp) : evt.timestamp ?? Date.now();
|
|
39
|
+
broadcast({
|
|
40
|
+
kind: "metadata-change",
|
|
41
|
+
type: evt.type ?? "changed",
|
|
42
|
+
metadataType: evt.metadataType ?? type,
|
|
43
|
+
name: evt.name ?? "",
|
|
44
|
+
path: evt.path,
|
|
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 } : {}
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
fsHookInstalled = true;
|
|
55
|
+
};
|
|
56
|
+
installFsHooks().catch(() => {
|
|
57
|
+
});
|
|
58
|
+
let onPostReload = null;
|
|
59
|
+
app.get(routePath, async (c) => {
|
|
60
|
+
await installFsHooks().catch(() => {
|
|
61
|
+
});
|
|
62
|
+
const types = await manager.getRegisteredTypes().catch(() => []);
|
|
63
|
+
const stream = new ReadableStream({
|
|
64
|
+
async start(controller) {
|
|
65
|
+
const enc = new TextEncoder();
|
|
66
|
+
let closed = false;
|
|
67
|
+
const safeEnqueue = (chunk) => {
|
|
68
|
+
if (closed) return;
|
|
69
|
+
try {
|
|
70
|
+
controller.enqueue(enc.encode(chunk));
|
|
71
|
+
} catch {
|
|
72
|
+
closed = true;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const listener = (evt) => {
|
|
76
|
+
if (closed) return;
|
|
77
|
+
const eventName = evt.kind === "reload" ? "reload" : "metadata-change";
|
|
78
|
+
safeEnqueue(`event: ${eventName}
|
|
79
|
+
data: ${JSON.stringify(evt)}
|
|
80
|
+
|
|
81
|
+
`);
|
|
82
|
+
};
|
|
83
|
+
listeners.add(listener);
|
|
84
|
+
safeEnqueue(`event: ready
|
|
85
|
+
data: ${JSON.stringify({ types, timestamp: Date.now() })}
|
|
86
|
+
|
|
87
|
+
`);
|
|
88
|
+
const heartbeat = setInterval(() => {
|
|
89
|
+
safeEnqueue(`: ping ${Date.now()}
|
|
90
|
+
|
|
91
|
+
`);
|
|
92
|
+
}, 15e3);
|
|
93
|
+
const cleanup = () => {
|
|
94
|
+
if (closed) return;
|
|
95
|
+
closed = true;
|
|
96
|
+
clearInterval(heartbeat);
|
|
97
|
+
listeners.delete(listener);
|
|
98
|
+
try {
|
|
99
|
+
controller.close();
|
|
100
|
+
} catch {
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
const signal = c.req?.raw?.signal;
|
|
104
|
+
if (signal) {
|
|
105
|
+
if (signal.aborted) cleanup();
|
|
106
|
+
else signal.addEventListener("abort", cleanup, { once: true });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
return new Response(stream, {
|
|
111
|
+
status: 200,
|
|
112
|
+
headers: {
|
|
113
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
114
|
+
"Cache-Control": "no-cache, no-transform",
|
|
115
|
+
"Connection": "keep-alive",
|
|
116
|
+
"X-Accel-Buffering": "no"
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
app.post(routePath, async (c) => {
|
|
121
|
+
let body = {};
|
|
122
|
+
try {
|
|
123
|
+
const ct = c.req?.header?.("content-type") ?? "";
|
|
124
|
+
if (typeof c.req?.json === "function" && ct.includes("json")) {
|
|
125
|
+
body = await c.req.json();
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
if (onPostReload) await onPostReload(body);
|
|
131
|
+
} catch (e) {
|
|
132
|
+
return new Response(
|
|
133
|
+
JSON.stringify({ ok: false, error: e?.message ?? "reload handler failed" }),
|
|
134
|
+
{ status: 500, headers: { "Content-Type": "application/json" } }
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
const reason = body.reason ?? "manual-trigger";
|
|
138
|
+
broadcast({
|
|
139
|
+
kind: "reload",
|
|
140
|
+
reason,
|
|
141
|
+
changed: body.changed,
|
|
142
|
+
timestamp: Date.now()
|
|
143
|
+
});
|
|
144
|
+
return new Response(
|
|
145
|
+
JSON.stringify({ ok: true, listeners: listeners.size, reason }),
|
|
146
|
+
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
return {
|
|
150
|
+
broadcastReload(reason, changed) {
|
|
151
|
+
broadcast({ kind: "reload", reason, changed, timestamp: Date.now() });
|
|
152
|
+
},
|
|
153
|
+
setOnPostReload(fn) {
|
|
154
|
+
onPostReload = fn;
|
|
155
|
+
},
|
|
156
|
+
listenerCount: () => listeners.size
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
var init_hmr_routes = __esm({
|
|
160
|
+
"src/routes/hmr-routes.ts"() {
|
|
161
|
+
"use strict";
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
7
165
|
// src/metadata-manager.ts
|
|
8
166
|
import { createLogger } from "@objectstack/core";
|
|
9
167
|
|
|
@@ -406,7 +564,7 @@ var DatabaseLoader = class {
|
|
|
406
564
|
this.tableName = options.tableName ?? "sys_metadata";
|
|
407
565
|
this.historyTableName = options.historyTableName ?? "sys_metadata_history";
|
|
408
566
|
this.organizationId = options.organizationId;
|
|
409
|
-
|
|
567
|
+
void options.projectId;
|
|
410
568
|
this.trackHistory = options.trackHistory !== false;
|
|
411
569
|
const cacheOpts = options.cache;
|
|
412
570
|
const cacheEnabled = cacheOpts?.enabled !== false;
|
|
@@ -496,6 +654,26 @@ var DatabaseLoader = class {
|
|
|
496
654
|
}
|
|
497
655
|
return this.driver.delete(table, id);
|
|
498
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
|
+
}
|
|
499
677
|
/**
|
|
500
678
|
* Ensure the metadata table exists.
|
|
501
679
|
* Uses IDataDriver.syncSchema with the SysMetadataObject definition
|
|
@@ -560,8 +738,9 @@ var DatabaseLoader = class {
|
|
|
560
738
|
}
|
|
561
739
|
/**
|
|
562
740
|
* Build base filter conditions for queries.
|
|
563
|
-
* Filters by organizationId when configured
|
|
564
|
-
*
|
|
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).
|
|
565
744
|
*/
|
|
566
745
|
baseFilter(type, name) {
|
|
567
746
|
const filter = { type };
|
|
@@ -571,7 +750,6 @@ var DatabaseLoader = class {
|
|
|
571
750
|
if (this.organizationId) {
|
|
572
751
|
filter.organization_id = this.organizationId;
|
|
573
752
|
}
|
|
574
|
-
filter.project_id = this.projectId ?? null;
|
|
575
753
|
return filter;
|
|
576
754
|
}
|
|
577
755
|
/**
|
|
@@ -597,6 +775,7 @@ var DatabaseLoader = class {
|
|
|
597
775
|
}
|
|
598
776
|
const historyId = generateId();
|
|
599
777
|
const metadataJson = JSON.stringify(metadata);
|
|
778
|
+
const eventSeq = await this.nextEventSeq();
|
|
600
779
|
const historyRecord = {
|
|
601
780
|
id: historyId,
|
|
602
781
|
metadataId,
|
|
@@ -610,12 +789,12 @@ var DatabaseLoader = class {
|
|
|
610
789
|
changeNote,
|
|
611
790
|
recordedBy,
|
|
612
791
|
recordedAt: now,
|
|
613
|
-
...this.organizationId ? { organizationId: this.organizationId } : {}
|
|
614
|
-
...this.projectId !== void 0 ? { projectId: this.projectId } : {}
|
|
792
|
+
...this.organizationId ? { organizationId: this.organizationId } : {}
|
|
615
793
|
};
|
|
616
794
|
try {
|
|
617
795
|
await this._create(this.historyTableName, {
|
|
618
796
|
id: historyRecord.id,
|
|
797
|
+
event_seq: eventSeq,
|
|
619
798
|
metadata_id: historyRecord.metadataId,
|
|
620
799
|
name: historyRecord.name,
|
|
621
800
|
type: historyRecord.type,
|
|
@@ -627,8 +806,8 @@ var DatabaseLoader = class {
|
|
|
627
806
|
change_note: historyRecord.changeNote,
|
|
628
807
|
recorded_by: historyRecord.recordedBy,
|
|
629
808
|
recorded_at: historyRecord.recordedAt,
|
|
630
|
-
|
|
631
|
-
...this.
|
|
809
|
+
source: "database-loader",
|
|
810
|
+
...this.organizationId ? { organization_id: this.organizationId } : {}
|
|
632
811
|
});
|
|
633
812
|
} catch (error) {
|
|
634
813
|
console.error(`Failed to create history record for ${type}/${name}:`, error);
|
|
@@ -815,7 +994,6 @@ var DatabaseLoader = class {
|
|
|
815
994
|
if (this.organizationId) {
|
|
816
995
|
filter.organization_id = this.organizationId;
|
|
817
996
|
}
|
|
818
|
-
filter.project_id = this.projectId ?? null;
|
|
819
997
|
const row = await this._findOne(this.historyTableName, {
|
|
820
998
|
where: filter
|
|
821
999
|
});
|
|
@@ -832,7 +1010,6 @@ var DatabaseLoader = class {
|
|
|
832
1010
|
previousChecksum: row.previous_checksum,
|
|
833
1011
|
changeNote: row.change_note,
|
|
834
1012
|
organizationId: row.organization_id,
|
|
835
|
-
projectId: row.project_id,
|
|
836
1013
|
recordedBy: row.recorded_by,
|
|
837
1014
|
recordedAt: row.recorded_at
|
|
838
1015
|
};
|
|
@@ -850,7 +1027,6 @@ var DatabaseLoader = class {
|
|
|
850
1027
|
await this.ensureHistorySchema();
|
|
851
1028
|
const filter = { type, name };
|
|
852
1029
|
if (this.organizationId) filter.organization_id = this.organizationId;
|
|
853
|
-
filter.project_id = this.projectId ?? null;
|
|
854
1030
|
const metadataRecord = await this._findOne(this.tableName, { where: filter });
|
|
855
1031
|
if (!metadataRecord) {
|
|
856
1032
|
return { records: [], total: 0, hasMore: false };
|
|
@@ -859,7 +1035,6 @@ var DatabaseLoader = class {
|
|
|
859
1035
|
metadata_id: metadataRecord.id
|
|
860
1036
|
};
|
|
861
1037
|
if (this.organizationId) historyFilter.organization_id = this.organizationId;
|
|
862
|
-
historyFilter.project_id = this.projectId ?? null;
|
|
863
1038
|
if (options?.operationType) historyFilter.operation_type = options.operationType;
|
|
864
1039
|
if (options?.since) historyFilter.recorded_at = { $gte: options.since };
|
|
865
1040
|
if (options?.until) {
|
|
@@ -898,7 +1073,6 @@ var DatabaseLoader = class {
|
|
|
898
1073
|
previousChecksum: row.previous_checksum,
|
|
899
1074
|
changeNote: row.change_note,
|
|
900
1075
|
organizationId: row.organization_id,
|
|
901
|
-
projectId: row.project_id,
|
|
902
1076
|
recordedBy: row.recorded_by,
|
|
903
1077
|
recordedAt: row.recorded_at
|
|
904
1078
|
};
|
|
@@ -1005,7 +1179,6 @@ var DatabaseLoader = class {
|
|
|
1005
1179
|
version: 1,
|
|
1006
1180
|
source: "database",
|
|
1007
1181
|
...this.organizationId ? { organization_id: this.organizationId } : {},
|
|
1008
|
-
...this.projectId !== void 0 ? { project_id: this.projectId } : { project_id: null },
|
|
1009
1182
|
created_at: now,
|
|
1010
1183
|
updated_at: now
|
|
1011
1184
|
});
|
|
@@ -1078,6 +1251,7 @@ var _MetadataManager = class _MetadataManager {
|
|
|
1078
1251
|
// Invalidated on every `register()` / `unregister()` to keep CRUD writes
|
|
1079
1252
|
// visible to subsequent reads.
|
|
1080
1253
|
this.listCache = /* @__PURE__ */ new Map();
|
|
1254
|
+
this.repoWatchClosed = false;
|
|
1081
1255
|
this.config = config;
|
|
1082
1256
|
this.logger = createLogger({ level: "info", format: "pretty" });
|
|
1083
1257
|
this.serializers = /* @__PURE__ */ new Map();
|
|
@@ -1773,6 +1947,21 @@ var _MetadataManager = class _MetadataManager {
|
|
|
1773
1947
|
unsubscribe: () => this.removeWatchCallback(type, wrappedCallback)
|
|
1774
1948
|
};
|
|
1775
1949
|
}
|
|
1950
|
+
/**
|
|
1951
|
+
* Subscribe to raw metadata watch events for a given type.
|
|
1952
|
+
*
|
|
1953
|
+
* Unlike `watchService` (which maps to the IMetadataService contract and
|
|
1954
|
+
* drops fields like `path`/`timestamp`), this returns the raw
|
|
1955
|
+
* `MetadataWatchEvent` produced by the underlying watcher — useful for
|
|
1956
|
+
* developer-facing tooling such as the HMR SSE endpoint that wants the
|
|
1957
|
+
* source file path and original timestamp.
|
|
1958
|
+
*
|
|
1959
|
+
* @returns An unsubscribe function.
|
|
1960
|
+
*/
|
|
1961
|
+
subscribe(type, callback) {
|
|
1962
|
+
this.addWatchCallback(type, callback);
|
|
1963
|
+
return () => this.removeWatchCallback(type, callback);
|
|
1964
|
+
}
|
|
1776
1965
|
// ==========================================
|
|
1777
1966
|
// Import / Export
|
|
1778
1967
|
// ==========================================
|
|
@@ -2070,6 +2259,110 @@ var _MetadataManager = class _MetadataManager {
|
|
|
2070
2259
|
*/
|
|
2071
2260
|
async stopWatching() {
|
|
2072
2261
|
}
|
|
2262
|
+
// ─── ADR-0008 PR-6: Repository wiring ───────────────────────────────
|
|
2263
|
+
/**
|
|
2264
|
+
* Attach a {@link MetadataRepository} as a supplementary event source.
|
|
2265
|
+
*
|
|
2266
|
+
* The manager subscribes to `repo.watch({})` and re-emits each event
|
|
2267
|
+
* through {@link notifyWatchers} as a legacy `MetadataWatchEvent`.
|
|
2268
|
+
* Each event also invalidates the in-memory registry entry and the
|
|
2269
|
+
* `list()` cache for the affected type so subsequent reads see fresh
|
|
2270
|
+
* data.
|
|
2271
|
+
*
|
|
2272
|
+
* No write-through. `register()` / `unregister()` / `save()` are
|
|
2273
|
+
* untouched in this PR (deferred to ADR-0008 M0 PR-10).
|
|
2274
|
+
*
|
|
2275
|
+
* Call {@link dispose} (or {@link stopRepositoryWatch}) to detach.
|
|
2276
|
+
*/
|
|
2277
|
+
setRepository(repo) {
|
|
2278
|
+
if (this.repository === repo) return;
|
|
2279
|
+
if (this.repository) {
|
|
2280
|
+
void this.stopRepositoryWatch();
|
|
2281
|
+
}
|
|
2282
|
+
this.repository = repo;
|
|
2283
|
+
this.repoWatchClosed = false;
|
|
2284
|
+
void this.startRepositoryWatch();
|
|
2285
|
+
}
|
|
2286
|
+
/** Return the attached repository, if any. */
|
|
2287
|
+
getRepository() {
|
|
2288
|
+
return this.repository;
|
|
2289
|
+
}
|
|
2290
|
+
/** Stop the active repo.watch() loop (best-effort). */
|
|
2291
|
+
async stopRepositoryWatch() {
|
|
2292
|
+
this.repoWatchClosed = true;
|
|
2293
|
+
const iter = this.repoWatchIter;
|
|
2294
|
+
this.repoWatchIter = void 0;
|
|
2295
|
+
if (iter && typeof iter.return === "function") {
|
|
2296
|
+
try {
|
|
2297
|
+
await iter.return(void 0);
|
|
2298
|
+
} catch {
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
/**
|
|
2303
|
+
* Best-effort cleanup. Stops the FS watcher (if any), drains the
|
|
2304
|
+
* repository watch loop, and clears registry caches. Safe to call
|
|
2305
|
+
* multiple times.
|
|
2306
|
+
*/
|
|
2307
|
+
async dispose() {
|
|
2308
|
+
await this.stopWatching().catch(() => void 0);
|
|
2309
|
+
await this.stopRepositoryWatch().catch(() => void 0);
|
|
2310
|
+
this.listCache.clear();
|
|
2311
|
+
}
|
|
2312
|
+
async startRepositoryWatch() {
|
|
2313
|
+
const repo = this.repository;
|
|
2314
|
+
if (!repo) return;
|
|
2315
|
+
const iterable = repo.watch({});
|
|
2316
|
+
const iter = iterable[Symbol.asyncIterator]();
|
|
2317
|
+
this.repoWatchIter = iter;
|
|
2318
|
+
try {
|
|
2319
|
+
while (!this.repoWatchClosed) {
|
|
2320
|
+
const { value, done } = await iter.next();
|
|
2321
|
+
if (done) break;
|
|
2322
|
+
try {
|
|
2323
|
+
this.applyRepoEvent(value);
|
|
2324
|
+
} catch (err) {
|
|
2325
|
+
this.logger.warn("[MetadataManager] repo event handler failed", {
|
|
2326
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2327
|
+
});
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
} catch (err) {
|
|
2331
|
+
if (!this.repoWatchClosed) {
|
|
2332
|
+
this.logger.warn("[MetadataManager] repository watch loop exited unexpectedly", {
|
|
2333
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2334
|
+
});
|
|
2335
|
+
}
|
|
2336
|
+
} finally {
|
|
2337
|
+
if (this.repoWatchIter === iter) this.repoWatchIter = void 0;
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
/** Translate a repo event to the legacy MetadataWatchEvent + invalidate caches. */
|
|
2341
|
+
applyRepoEvent(evt) {
|
|
2342
|
+
const ref = evt.ref;
|
|
2343
|
+
const type = ref.type;
|
|
2344
|
+
const name = ref.name;
|
|
2345
|
+
const typeStore = this.registry.get(type);
|
|
2346
|
+
if (typeStore) {
|
|
2347
|
+
typeStore.delete(name);
|
|
2348
|
+
if (typeStore.size === 0) this.registry.delete(type);
|
|
2349
|
+
}
|
|
2350
|
+
this.listCache.delete(type);
|
|
2351
|
+
const legacyType = evt.op === "create" ? "added" : evt.op === "delete" ? "deleted" : "changed";
|
|
2352
|
+
const legacyEvent = {
|
|
2353
|
+
type: legacyType,
|
|
2354
|
+
metadataType: type,
|
|
2355
|
+
name,
|
|
2356
|
+
path: "",
|
|
2357
|
+
// Repo events carry the hash only; the body is fetched on demand
|
|
2358
|
+
// via manager.get(type, name). HMR consumers don't read `data` so
|
|
2359
|
+
// this is fine for M0. (See ADR-0008 §12 open question 1.)
|
|
2360
|
+
data: void 0,
|
|
2361
|
+
timestamp: evt.ts
|
|
2362
|
+
};
|
|
2363
|
+
legacyEvent.seq = evt.seq;
|
|
2364
|
+
this.notifyWatchers(type, legacyEvent);
|
|
2365
|
+
}
|
|
2073
2366
|
notifyWatchers(type, event) {
|
|
2074
2367
|
const callbacks = this.watchCallbacks.get(type);
|
|
2075
2368
|
if (!callbacks) return;
|
|
@@ -2545,7 +2838,13 @@ var NodeMetadataManager = class extends MetadataManager {
|
|
|
2545
2838
|
this.watcher = chokidarWatch(rootDir, {
|
|
2546
2839
|
ignored,
|
|
2547
2840
|
persistent,
|
|
2548
|
-
ignoreInitial: true
|
|
2841
|
+
ignoreInitial: true,
|
|
2842
|
+
// Use polling to avoid `fs.watch` EMFILE on macOS / busy dev hosts.
|
|
2843
|
+
// Recursive watch over a project root would otherwise wire native
|
|
2844
|
+
// watches across the entire tree, easily exhausting the FD pool.
|
|
2845
|
+
usePolling: true,
|
|
2846
|
+
interval: 1e3,
|
|
2847
|
+
binaryInterval: 2e3
|
|
2549
2848
|
});
|
|
2550
2849
|
this.watcher.on("add", async (filePath) => {
|
|
2551
2850
|
await this.handleFileEvent("added", filePath);
|
|
@@ -2683,6 +2982,7 @@ var queryableMetadataObjects = [
|
|
|
2683
2982
|
SysMetadataObject2,
|
|
2684
2983
|
SysMetadataHistoryObject2
|
|
2685
2984
|
];
|
|
2985
|
+
var REPO_SUBDIR = ".objectstack/metadata";
|
|
2686
2986
|
var ARTIFACT_FIELD_TO_TYPE = {
|
|
2687
2987
|
objects: "object",
|
|
2688
2988
|
objectExtensions: "object_extension",
|
|
@@ -2780,6 +3080,31 @@ var MetadataPlugin = class {
|
|
|
2780
3080
|
await this._loadFromFileSystem(ctx);
|
|
2781
3081
|
}
|
|
2782
3082
|
}
|
|
3083
|
+
const bootstrapMode = this.options.config?.bootstrap ?? "eager";
|
|
3084
|
+
if (bootstrapMode !== "artifact-only") {
|
|
3085
|
+
try {
|
|
3086
|
+
const path3 = await import("path");
|
|
3087
|
+
const { FileSystemRepository } = await import("@objectstack/metadata-fs");
|
|
3088
|
+
const rootDir = this.options.rootDir || process.cwd();
|
|
3089
|
+
const repoRoot = path3.join(rootDir, REPO_SUBDIR);
|
|
3090
|
+
const repo = new FileSystemRepository({
|
|
3091
|
+
root: repoRoot,
|
|
3092
|
+
org: this.options.organizationId ?? "system",
|
|
3093
|
+
disableWatch: this.options.watch === false
|
|
3094
|
+
});
|
|
3095
|
+
await repo.start();
|
|
3096
|
+
this.repository = repo;
|
|
3097
|
+
this.manager.setRepository(repo);
|
|
3098
|
+
ctx.logger.info("[MetadataPlugin] FileSystemRepository attached", {
|
|
3099
|
+
repoRoot,
|
|
3100
|
+
watch: this.options.watch !== false
|
|
3101
|
+
});
|
|
3102
|
+
} catch (e) {
|
|
3103
|
+
ctx.logger.warn("[MetadataPlugin] Failed to attach FileSystemRepository", {
|
|
3104
|
+
error: e?.message
|
|
3105
|
+
});
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
2783
3108
|
try {
|
|
2784
3109
|
const realtimeService = ctx.getService("realtime");
|
|
2785
3110
|
if (realtimeService && typeof realtimeService === "object" && "publish" in realtimeService) {
|
|
@@ -2791,6 +3116,102 @@ var MetadataPlugin = class {
|
|
|
2791
3116
|
error: e.message
|
|
2792
3117
|
});
|
|
2793
3118
|
}
|
|
3119
|
+
try {
|
|
3120
|
+
const httpServer = ctx.getService("http-server") ?? ctx.getService("http.server");
|
|
3121
|
+
if (httpServer && typeof httpServer.getRawApp === "function") {
|
|
3122
|
+
const { registerMetadataHmrRoutes: registerMetadataHmrRoutes2 } = await Promise.resolve().then(() => (init_hmr_routes(), hmr_routes_exports));
|
|
3123
|
+
const hub = registerMetadataHmrRoutes2(httpServer.getRawApp(), this.manager);
|
|
3124
|
+
hub.setOnPostReload(async (body = {}) => {
|
|
3125
|
+
const src3 = this.options.artifactSource;
|
|
3126
|
+
if (src3?.mode === "local-file") {
|
|
3127
|
+
try {
|
|
3128
|
+
await this._loadFromLocalFile(ctx, src3.path, src3.fetchTimeoutMs);
|
|
3129
|
+
ctx.logger.info("[MetadataPlugin] artifact reloaded via HMR POST", {
|
|
3130
|
+
path: src3.path,
|
|
3131
|
+
reason: body?.reason
|
|
3132
|
+
});
|
|
3133
|
+
} catch (e) {
|
|
3134
|
+
ctx.logger.warn("[MetadataPlugin] artifact reload failed", { error: e?.message });
|
|
3135
|
+
throw e;
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
});
|
|
3139
|
+
const src2 = this.options.artifactSource;
|
|
3140
|
+
const wantArtifactWatch = this.options.artifactWatch ?? src2?.mode === "local-file";
|
|
3141
|
+
if (src2?.mode === "local-file" && wantArtifactWatch && !/^https?:\/\//i.test(src2.path)) {
|
|
3142
|
+
try {
|
|
3143
|
+
const { watch: chokidarWatch2 } = await import("chokidar");
|
|
3144
|
+
const w = chokidarWatch2(src2.path, {
|
|
3145
|
+
ignoreInitial: true,
|
|
3146
|
+
awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 20 },
|
|
3147
|
+
persistent: true,
|
|
3148
|
+
// Use polling to avoid `fs.watch` exhausting the
|
|
3149
|
+
// process file-descriptor limit on macOS (chokidar
|
|
3150
|
+
// recursively wires watches on the parent
|
|
3151
|
+
// directory tree which can trip EMFILE on busy
|
|
3152
|
+
// dev hosts). 500ms polling is fast enough for
|
|
3153
|
+
// HMR (a recompile takes ~400ms anyway).
|
|
3154
|
+
usePolling: true,
|
|
3155
|
+
interval: 500,
|
|
3156
|
+
binaryInterval: 1e3
|
|
3157
|
+
});
|
|
3158
|
+
let pending = false;
|
|
3159
|
+
const reload = async () => {
|
|
3160
|
+
if (pending) return;
|
|
3161
|
+
pending = true;
|
|
3162
|
+
try {
|
|
3163
|
+
await this._loadFromLocalFile(ctx, src2.path, src2.fetchTimeoutMs);
|
|
3164
|
+
hub.broadcastReload("artifact-file-changed", [src2.path]);
|
|
3165
|
+
ctx.logger.info("[MetadataPlugin] artifact auto-reloaded (file watcher)", {
|
|
3166
|
+
path: src2.path
|
|
3167
|
+
});
|
|
3168
|
+
} catch (e) {
|
|
3169
|
+
ctx.logger.warn("[MetadataPlugin] artifact auto-reload failed", { error: e?.message });
|
|
3170
|
+
} finally {
|
|
3171
|
+
pending = false;
|
|
3172
|
+
}
|
|
3173
|
+
};
|
|
3174
|
+
w.on("change", () => {
|
|
3175
|
+
void reload();
|
|
3176
|
+
});
|
|
3177
|
+
w.on("add", () => {
|
|
3178
|
+
void reload();
|
|
3179
|
+
});
|
|
3180
|
+
this.artifactWatcher = { close: () => w.close() };
|
|
3181
|
+
console.log("[MetadataPlugin] artifact file watcher attached", src2.path);
|
|
3182
|
+
} catch (e) {
|
|
3183
|
+
ctx.logger.warn("[MetadataPlugin] artifact watcher failed to start", { error: e?.message });
|
|
3184
|
+
}
|
|
3185
|
+
}
|
|
3186
|
+
console.log("[MetadataPlugin] HMR endpoint registered at /api/v1/dev/metadata-events");
|
|
3187
|
+
} else {
|
|
3188
|
+
console.log("[MetadataPlugin] HTTP server with getRawApp() not available \u2014 skipping HMR endpoint");
|
|
3189
|
+
}
|
|
3190
|
+
} catch (e) {
|
|
3191
|
+
console.warn("[MetadataPlugin] Failed to register HMR endpoint", e?.message);
|
|
3192
|
+
}
|
|
3193
|
+
};
|
|
3194
|
+
this.stop = async (ctx) => {
|
|
3195
|
+
if (this.artifactWatcher) {
|
|
3196
|
+
try {
|
|
3197
|
+
await this.artifactWatcher.close();
|
|
3198
|
+
} catch {
|
|
3199
|
+
}
|
|
3200
|
+
this.artifactWatcher = void 0;
|
|
3201
|
+
}
|
|
3202
|
+
try {
|
|
3203
|
+
await this.manager.dispose();
|
|
3204
|
+
} catch (e) {
|
|
3205
|
+
ctx.logger.warn("[MetadataPlugin] manager.dispose() failed", { error: e?.message });
|
|
3206
|
+
}
|
|
3207
|
+
const repo = this.repository;
|
|
3208
|
+
if (repo && typeof repo.close === "function") {
|
|
3209
|
+
try {
|
|
3210
|
+
await repo.close();
|
|
3211
|
+
} catch {
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
this.repository = void 0;
|
|
2794
3215
|
};
|
|
2795
3216
|
this.options = {
|
|
2796
3217
|
watch: true,
|
|
@@ -2868,7 +3289,12 @@ var MetadataPlugin = class {
|
|
|
2868
3289
|
const items = metadata[field];
|
|
2869
3290
|
if (!Array.isArray(items) || items.length === 0) continue;
|
|
2870
3291
|
for (const item of items) {
|
|
2871
|
-
|
|
3292
|
+
let name = item?.name;
|
|
3293
|
+
if (!name) {
|
|
3294
|
+
if (metaType === "view") {
|
|
3295
|
+
name = item?.list?.data?.object ?? item?.form?.data?.object;
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
2872
3298
|
if (!name) continue;
|
|
2873
3299
|
if (manifestPackageId && item._packageId === void 0) {
|
|
2874
3300
|
item._packageId = manifestPackageId;
|
|
@@ -3212,7 +3638,6 @@ var HistoryCleanupManager = class {
|
|
|
3212
3638
|
const driver = this.dbLoader.driver;
|
|
3213
3639
|
const historyTableName = this.dbLoader.historyTableName;
|
|
3214
3640
|
const organizationId = this.dbLoader.organizationId;
|
|
3215
|
-
const projectId = this.dbLoader.projectId;
|
|
3216
3641
|
let deleted = 0;
|
|
3217
3642
|
let errors = 0;
|
|
3218
3643
|
try {
|
|
@@ -3226,9 +3651,6 @@ var HistoryCleanupManager = class {
|
|
|
3226
3651
|
if (organizationId) {
|
|
3227
3652
|
filter.organization_id = organizationId;
|
|
3228
3653
|
}
|
|
3229
|
-
if (projectId !== void 0) {
|
|
3230
|
-
filter.project_id = projectId;
|
|
3231
|
-
}
|
|
3232
3654
|
try {
|
|
3233
3655
|
const result = await this.bulkDeleteByFilter(driver, historyTableName, filter);
|
|
3234
3656
|
deleted += result.deleted;
|
|
@@ -3241,7 +3663,6 @@ var HistoryCleanupManager = class {
|
|
|
3241
3663
|
try {
|
|
3242
3664
|
const baseWhere = {};
|
|
3243
3665
|
if (organizationId) baseWhere.organization_id = organizationId;
|
|
3244
|
-
if (projectId !== void 0) baseWhere.project_id = projectId;
|
|
3245
3666
|
const metadataIds = await driver.find(historyTableName, {
|
|
3246
3667
|
object: historyTableName,
|
|
3247
3668
|
where: baseWhere,
|
|
@@ -3329,13 +3750,11 @@ var HistoryCleanupManager = class {
|
|
|
3329
3750
|
const driver = this.dbLoader.driver;
|
|
3330
3751
|
const historyTableName = this.dbLoader.historyTableName;
|
|
3331
3752
|
const organizationId = this.dbLoader.organizationId;
|
|
3332
|
-
const projectId = this.dbLoader.projectId;
|
|
3333
3753
|
let recordsByAge = 0;
|
|
3334
3754
|
let recordsByCount = 0;
|
|
3335
3755
|
try {
|
|
3336
3756
|
const baseWhere = {};
|
|
3337
3757
|
if (organizationId) baseWhere.organization_id = organizationId;
|
|
3338
|
-
if (projectId !== void 0) baseWhere.project_id = projectId;
|
|
3339
3758
|
if (this.policy.maxAgeDays) {
|
|
3340
3759
|
const cutoffDate = /* @__PURE__ */ new Date();
|
|
3341
3760
|
cutoffDate.setDate(cutoffDate.getDate() - this.policy.maxAgeDays);
|