@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/node.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/node.ts
|
|
31
188
|
var node_exports = {};
|
|
32
189
|
__export(node_exports, {
|
|
@@ -453,7 +610,7 @@ var DatabaseLoader = class {
|
|
|
453
610
|
this.tableName = options.tableName ?? "sys_metadata";
|
|
454
611
|
this.historyTableName = options.historyTableName ?? "sys_metadata_history";
|
|
455
612
|
this.organizationId = options.organizationId;
|
|
456
|
-
|
|
613
|
+
void options.projectId;
|
|
457
614
|
this.trackHistory = options.trackHistory !== false;
|
|
458
615
|
const cacheOpts = options.cache;
|
|
459
616
|
const cacheEnabled = cacheOpts?.enabled !== false;
|
|
@@ -543,6 +700,26 @@ var DatabaseLoader = class {
|
|
|
543
700
|
}
|
|
544
701
|
return this.driver.delete(table, id);
|
|
545
702
|
}
|
|
703
|
+
/**
|
|
704
|
+
* Compute the next per-org `event_seq` for `sys_metadata_history`.
|
|
705
|
+
* Reads `MAX(event_seq) + 1` for the configured `organization_id`.
|
|
706
|
+
* Legacy path — not transactional, so concurrent writes can collide.
|
|
707
|
+
* The canonical (transactional) producer is `SysMetadataRepository`.
|
|
708
|
+
*/
|
|
709
|
+
async nextEventSeq() {
|
|
710
|
+
const where = this.organizationId ? { organization_id: this.organizationId } : {};
|
|
711
|
+
try {
|
|
712
|
+
const rows = await this._find(this.historyTableName, { where });
|
|
713
|
+
let max = 0;
|
|
714
|
+
for (const row of rows) {
|
|
715
|
+
const v = typeof row.event_seq === "number" ? row.event_seq : 0;
|
|
716
|
+
if (v > max) max = v;
|
|
717
|
+
}
|
|
718
|
+
return max + 1;
|
|
719
|
+
} catch {
|
|
720
|
+
return 1;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
546
723
|
/**
|
|
547
724
|
* Ensure the metadata table exists.
|
|
548
725
|
* Uses IDataDriver.syncSchema with the SysMetadataObject definition
|
|
@@ -607,8 +784,9 @@ var DatabaseLoader = class {
|
|
|
607
784
|
}
|
|
608
785
|
/**
|
|
609
786
|
* Build base filter conditions for queries.
|
|
610
|
-
* Filters by organizationId when configured
|
|
611
|
-
*
|
|
787
|
+
* Filters by organizationId when configured. `projectId` is accepted
|
|
788
|
+
* for back-compat but no longer constrains the query — see
|
|
789
|
+
* ADR-0008 §0 (branch/project removal).
|
|
612
790
|
*/
|
|
613
791
|
baseFilter(type, name) {
|
|
614
792
|
const filter = { type };
|
|
@@ -618,7 +796,6 @@ var DatabaseLoader = class {
|
|
|
618
796
|
if (this.organizationId) {
|
|
619
797
|
filter.organization_id = this.organizationId;
|
|
620
798
|
}
|
|
621
|
-
filter.project_id = this.projectId ?? null;
|
|
622
799
|
return filter;
|
|
623
800
|
}
|
|
624
801
|
/**
|
|
@@ -644,6 +821,7 @@ var DatabaseLoader = class {
|
|
|
644
821
|
}
|
|
645
822
|
const historyId = generateId();
|
|
646
823
|
const metadataJson = JSON.stringify(metadata);
|
|
824
|
+
const eventSeq = await this.nextEventSeq();
|
|
647
825
|
const historyRecord = {
|
|
648
826
|
id: historyId,
|
|
649
827
|
metadataId,
|
|
@@ -657,12 +835,12 @@ var DatabaseLoader = class {
|
|
|
657
835
|
changeNote,
|
|
658
836
|
recordedBy,
|
|
659
837
|
recordedAt: now,
|
|
660
|
-
...this.organizationId ? { organizationId: this.organizationId } : {}
|
|
661
|
-
...this.projectId !== void 0 ? { projectId: this.projectId } : {}
|
|
838
|
+
...this.organizationId ? { organizationId: this.organizationId } : {}
|
|
662
839
|
};
|
|
663
840
|
try {
|
|
664
841
|
await this._create(this.historyTableName, {
|
|
665
842
|
id: historyRecord.id,
|
|
843
|
+
event_seq: eventSeq,
|
|
666
844
|
metadata_id: historyRecord.metadataId,
|
|
667
845
|
name: historyRecord.name,
|
|
668
846
|
type: historyRecord.type,
|
|
@@ -674,8 +852,8 @@ var DatabaseLoader = class {
|
|
|
674
852
|
change_note: historyRecord.changeNote,
|
|
675
853
|
recorded_by: historyRecord.recordedBy,
|
|
676
854
|
recorded_at: historyRecord.recordedAt,
|
|
677
|
-
|
|
678
|
-
...this.
|
|
855
|
+
source: "database-loader",
|
|
856
|
+
...this.organizationId ? { organization_id: this.organizationId } : {}
|
|
679
857
|
});
|
|
680
858
|
} catch (error) {
|
|
681
859
|
console.error(`Failed to create history record for ${type}/${name}:`, error);
|
|
@@ -862,7 +1040,6 @@ var DatabaseLoader = class {
|
|
|
862
1040
|
if (this.organizationId) {
|
|
863
1041
|
filter.organization_id = this.organizationId;
|
|
864
1042
|
}
|
|
865
|
-
filter.project_id = this.projectId ?? null;
|
|
866
1043
|
const row = await this._findOne(this.historyTableName, {
|
|
867
1044
|
where: filter
|
|
868
1045
|
});
|
|
@@ -879,7 +1056,6 @@ var DatabaseLoader = class {
|
|
|
879
1056
|
previousChecksum: row.previous_checksum,
|
|
880
1057
|
changeNote: row.change_note,
|
|
881
1058
|
organizationId: row.organization_id,
|
|
882
|
-
projectId: row.project_id,
|
|
883
1059
|
recordedBy: row.recorded_by,
|
|
884
1060
|
recordedAt: row.recorded_at
|
|
885
1061
|
};
|
|
@@ -897,7 +1073,6 @@ var DatabaseLoader = class {
|
|
|
897
1073
|
await this.ensureHistorySchema();
|
|
898
1074
|
const filter = { type, name };
|
|
899
1075
|
if (this.organizationId) filter.organization_id = this.organizationId;
|
|
900
|
-
filter.project_id = this.projectId ?? null;
|
|
901
1076
|
const metadataRecord = await this._findOne(this.tableName, { where: filter });
|
|
902
1077
|
if (!metadataRecord) {
|
|
903
1078
|
return { records: [], total: 0, hasMore: false };
|
|
@@ -906,7 +1081,6 @@ var DatabaseLoader = class {
|
|
|
906
1081
|
metadata_id: metadataRecord.id
|
|
907
1082
|
};
|
|
908
1083
|
if (this.organizationId) historyFilter.organization_id = this.organizationId;
|
|
909
|
-
historyFilter.project_id = this.projectId ?? null;
|
|
910
1084
|
if (options?.operationType) historyFilter.operation_type = options.operationType;
|
|
911
1085
|
if (options?.since) historyFilter.recorded_at = { $gte: options.since };
|
|
912
1086
|
if (options?.until) {
|
|
@@ -945,7 +1119,6 @@ var DatabaseLoader = class {
|
|
|
945
1119
|
previousChecksum: row.previous_checksum,
|
|
946
1120
|
changeNote: row.change_note,
|
|
947
1121
|
organizationId: row.organization_id,
|
|
948
|
-
projectId: row.project_id,
|
|
949
1122
|
recordedBy: row.recorded_by,
|
|
950
1123
|
recordedAt: row.recorded_at
|
|
951
1124
|
};
|
|
@@ -1052,7 +1225,6 @@ var DatabaseLoader = class {
|
|
|
1052
1225
|
version: 1,
|
|
1053
1226
|
source: "database",
|
|
1054
1227
|
...this.organizationId ? { organization_id: this.organizationId } : {},
|
|
1055
|
-
...this.projectId !== void 0 ? { project_id: this.projectId } : { project_id: null },
|
|
1056
1228
|
created_at: now,
|
|
1057
1229
|
updated_at: now
|
|
1058
1230
|
});
|
|
@@ -1125,6 +1297,7 @@ var _MetadataManager = class _MetadataManager {
|
|
|
1125
1297
|
// Invalidated on every `register()` / `unregister()` to keep CRUD writes
|
|
1126
1298
|
// visible to subsequent reads.
|
|
1127
1299
|
this.listCache = /* @__PURE__ */ new Map();
|
|
1300
|
+
this.repoWatchClosed = false;
|
|
1128
1301
|
this.config = config;
|
|
1129
1302
|
this.logger = (0, import_core.createLogger)({ level: "info", format: "pretty" });
|
|
1130
1303
|
this.serializers = /* @__PURE__ */ new Map();
|
|
@@ -1820,6 +1993,21 @@ var _MetadataManager = class _MetadataManager {
|
|
|
1820
1993
|
unsubscribe: () => this.removeWatchCallback(type, wrappedCallback)
|
|
1821
1994
|
};
|
|
1822
1995
|
}
|
|
1996
|
+
/**
|
|
1997
|
+
* Subscribe to raw metadata watch events for a given type.
|
|
1998
|
+
*
|
|
1999
|
+
* Unlike `watchService` (which maps to the IMetadataService contract and
|
|
2000
|
+
* drops fields like `path`/`timestamp`), this returns the raw
|
|
2001
|
+
* `MetadataWatchEvent` produced by the underlying watcher — useful for
|
|
2002
|
+
* developer-facing tooling such as the HMR SSE endpoint that wants the
|
|
2003
|
+
* source file path and original timestamp.
|
|
2004
|
+
*
|
|
2005
|
+
* @returns An unsubscribe function.
|
|
2006
|
+
*/
|
|
2007
|
+
subscribe(type, callback) {
|
|
2008
|
+
this.addWatchCallback(type, callback);
|
|
2009
|
+
return () => this.removeWatchCallback(type, callback);
|
|
2010
|
+
}
|
|
1823
2011
|
// ==========================================
|
|
1824
2012
|
// Import / Export
|
|
1825
2013
|
// ==========================================
|
|
@@ -2117,6 +2305,110 @@ var _MetadataManager = class _MetadataManager {
|
|
|
2117
2305
|
*/
|
|
2118
2306
|
async stopWatching() {
|
|
2119
2307
|
}
|
|
2308
|
+
// ─── ADR-0008 PR-6: Repository wiring ───────────────────────────────
|
|
2309
|
+
/**
|
|
2310
|
+
* Attach a {@link MetadataRepository} as a supplementary event source.
|
|
2311
|
+
*
|
|
2312
|
+
* The manager subscribes to `repo.watch({})` and re-emits each event
|
|
2313
|
+
* through {@link notifyWatchers} as a legacy `MetadataWatchEvent`.
|
|
2314
|
+
* Each event also invalidates the in-memory registry entry and the
|
|
2315
|
+
* `list()` cache for the affected type so subsequent reads see fresh
|
|
2316
|
+
* data.
|
|
2317
|
+
*
|
|
2318
|
+
* No write-through. `register()` / `unregister()` / `save()` are
|
|
2319
|
+
* untouched in this PR (deferred to ADR-0008 M0 PR-10).
|
|
2320
|
+
*
|
|
2321
|
+
* Call {@link dispose} (or {@link stopRepositoryWatch}) to detach.
|
|
2322
|
+
*/
|
|
2323
|
+
setRepository(repo) {
|
|
2324
|
+
if (this.repository === repo) return;
|
|
2325
|
+
if (this.repository) {
|
|
2326
|
+
void this.stopRepositoryWatch();
|
|
2327
|
+
}
|
|
2328
|
+
this.repository = repo;
|
|
2329
|
+
this.repoWatchClosed = false;
|
|
2330
|
+
void this.startRepositoryWatch();
|
|
2331
|
+
}
|
|
2332
|
+
/** Return the attached repository, if any. */
|
|
2333
|
+
getRepository() {
|
|
2334
|
+
return this.repository;
|
|
2335
|
+
}
|
|
2336
|
+
/** Stop the active repo.watch() loop (best-effort). */
|
|
2337
|
+
async stopRepositoryWatch() {
|
|
2338
|
+
this.repoWatchClosed = true;
|
|
2339
|
+
const iter = this.repoWatchIter;
|
|
2340
|
+
this.repoWatchIter = void 0;
|
|
2341
|
+
if (iter && typeof iter.return === "function") {
|
|
2342
|
+
try {
|
|
2343
|
+
await iter.return(void 0);
|
|
2344
|
+
} catch {
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
/**
|
|
2349
|
+
* Best-effort cleanup. Stops the FS watcher (if any), drains the
|
|
2350
|
+
* repository watch loop, and clears registry caches. Safe to call
|
|
2351
|
+
* multiple times.
|
|
2352
|
+
*/
|
|
2353
|
+
async dispose() {
|
|
2354
|
+
await this.stopWatching().catch(() => void 0);
|
|
2355
|
+
await this.stopRepositoryWatch().catch(() => void 0);
|
|
2356
|
+
this.listCache.clear();
|
|
2357
|
+
}
|
|
2358
|
+
async startRepositoryWatch() {
|
|
2359
|
+
const repo = this.repository;
|
|
2360
|
+
if (!repo) return;
|
|
2361
|
+
const iterable = repo.watch({});
|
|
2362
|
+
const iter = iterable[Symbol.asyncIterator]();
|
|
2363
|
+
this.repoWatchIter = iter;
|
|
2364
|
+
try {
|
|
2365
|
+
while (!this.repoWatchClosed) {
|
|
2366
|
+
const { value, done } = await iter.next();
|
|
2367
|
+
if (done) break;
|
|
2368
|
+
try {
|
|
2369
|
+
this.applyRepoEvent(value);
|
|
2370
|
+
} catch (err) {
|
|
2371
|
+
this.logger.warn("[MetadataManager] repo event handler failed", {
|
|
2372
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2373
|
+
});
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
} catch (err) {
|
|
2377
|
+
if (!this.repoWatchClosed) {
|
|
2378
|
+
this.logger.warn("[MetadataManager] repository watch loop exited unexpectedly", {
|
|
2379
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2382
|
+
} finally {
|
|
2383
|
+
if (this.repoWatchIter === iter) this.repoWatchIter = void 0;
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
/** Translate a repo event to the legacy MetadataWatchEvent + invalidate caches. */
|
|
2387
|
+
applyRepoEvent(evt) {
|
|
2388
|
+
const ref = evt.ref;
|
|
2389
|
+
const type = ref.type;
|
|
2390
|
+
const name = ref.name;
|
|
2391
|
+
const typeStore = this.registry.get(type);
|
|
2392
|
+
if (typeStore) {
|
|
2393
|
+
typeStore.delete(name);
|
|
2394
|
+
if (typeStore.size === 0) this.registry.delete(type);
|
|
2395
|
+
}
|
|
2396
|
+
this.listCache.delete(type);
|
|
2397
|
+
const legacyType = evt.op === "create" ? "added" : evt.op === "delete" ? "deleted" : "changed";
|
|
2398
|
+
const legacyEvent = {
|
|
2399
|
+
type: legacyType,
|
|
2400
|
+
metadataType: type,
|
|
2401
|
+
name,
|
|
2402
|
+
path: "",
|
|
2403
|
+
// Repo events carry the hash only; the body is fetched on demand
|
|
2404
|
+
// via manager.get(type, name). HMR consumers don't read `data` so
|
|
2405
|
+
// this is fine for M0. (See ADR-0008 §12 open question 1.)
|
|
2406
|
+
data: void 0,
|
|
2407
|
+
timestamp: evt.ts
|
|
2408
|
+
};
|
|
2409
|
+
legacyEvent.seq = evt.seq;
|
|
2410
|
+
this.notifyWatchers(type, legacyEvent);
|
|
2411
|
+
}
|
|
2120
2412
|
notifyWatchers(type, event) {
|
|
2121
2413
|
const callbacks = this.watchCallbacks.get(type);
|
|
2122
2414
|
if (!callbacks) return;
|
|
@@ -2592,7 +2884,13 @@ var NodeMetadataManager = class extends MetadataManager {
|
|
|
2592
2884
|
this.watcher = (0, import_chokidar.watch)(rootDir, {
|
|
2593
2885
|
ignored,
|
|
2594
2886
|
persistent,
|
|
2595
|
-
ignoreInitial: true
|
|
2887
|
+
ignoreInitial: true,
|
|
2888
|
+
// Use polling to avoid `fs.watch` EMFILE on macOS / busy dev hosts.
|
|
2889
|
+
// Recursive watch over a project root would otherwise wire native
|
|
2890
|
+
// watches across the entire tree, easily exhausting the FD pool.
|
|
2891
|
+
usePolling: true,
|
|
2892
|
+
interval: 1e3,
|
|
2893
|
+
binaryInterval: 2e3
|
|
2596
2894
|
});
|
|
2597
2895
|
this.watcher.on("add", async (filePath) => {
|
|
2598
2896
|
await this.handleFileEvent("added", filePath);
|
|
@@ -2727,6 +3025,7 @@ var queryableMetadataObjects = [
|
|
|
2727
3025
|
import_metadata2.SysMetadataObject,
|
|
2728
3026
|
import_metadata2.SysMetadataHistoryObject
|
|
2729
3027
|
];
|
|
3028
|
+
var REPO_SUBDIR = ".objectstack/metadata";
|
|
2730
3029
|
var ARTIFACT_FIELD_TO_TYPE = {
|
|
2731
3030
|
objects: "object",
|
|
2732
3031
|
objectExtensions: "object_extension",
|
|
@@ -2824,6 +3123,31 @@ var MetadataPlugin = class {
|
|
|
2824
3123
|
await this._loadFromFileSystem(ctx);
|
|
2825
3124
|
}
|
|
2826
3125
|
}
|
|
3126
|
+
const bootstrapMode = this.options.config?.bootstrap ?? "eager";
|
|
3127
|
+
if (bootstrapMode !== "artifact-only") {
|
|
3128
|
+
try {
|
|
3129
|
+
const path3 = await import("path");
|
|
3130
|
+
const { FileSystemRepository } = await import("@objectstack/metadata-fs");
|
|
3131
|
+
const rootDir = this.options.rootDir || process.cwd();
|
|
3132
|
+
const repoRoot = path3.join(rootDir, REPO_SUBDIR);
|
|
3133
|
+
const repo = new FileSystemRepository({
|
|
3134
|
+
root: repoRoot,
|
|
3135
|
+
org: this.options.organizationId ?? "system",
|
|
3136
|
+
disableWatch: this.options.watch === false
|
|
3137
|
+
});
|
|
3138
|
+
await repo.start();
|
|
3139
|
+
this.repository = repo;
|
|
3140
|
+
this.manager.setRepository(repo);
|
|
3141
|
+
ctx.logger.info("[MetadataPlugin] FileSystemRepository attached", {
|
|
3142
|
+
repoRoot,
|
|
3143
|
+
watch: this.options.watch !== false
|
|
3144
|
+
});
|
|
3145
|
+
} catch (e) {
|
|
3146
|
+
ctx.logger.warn("[MetadataPlugin] Failed to attach FileSystemRepository", {
|
|
3147
|
+
error: e?.message
|
|
3148
|
+
});
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
2827
3151
|
try {
|
|
2828
3152
|
const realtimeService = ctx.getService("realtime");
|
|
2829
3153
|
if (realtimeService && typeof realtimeService === "object" && "publish" in realtimeService) {
|
|
@@ -2835,6 +3159,102 @@ var MetadataPlugin = class {
|
|
|
2835
3159
|
error: e.message
|
|
2836
3160
|
});
|
|
2837
3161
|
}
|
|
3162
|
+
try {
|
|
3163
|
+
const httpServer = ctx.getService("http-server") ?? ctx.getService("http.server");
|
|
3164
|
+
if (httpServer && typeof httpServer.getRawApp === "function") {
|
|
3165
|
+
const { registerMetadataHmrRoutes: registerMetadataHmrRoutes2 } = await Promise.resolve().then(() => (init_hmr_routes(), hmr_routes_exports));
|
|
3166
|
+
const hub = registerMetadataHmrRoutes2(httpServer.getRawApp(), this.manager);
|
|
3167
|
+
hub.setOnPostReload(async (body = {}) => {
|
|
3168
|
+
const src3 = this.options.artifactSource;
|
|
3169
|
+
if (src3?.mode === "local-file") {
|
|
3170
|
+
try {
|
|
3171
|
+
await this._loadFromLocalFile(ctx, src3.path, src3.fetchTimeoutMs);
|
|
3172
|
+
ctx.logger.info("[MetadataPlugin] artifact reloaded via HMR POST", {
|
|
3173
|
+
path: src3.path,
|
|
3174
|
+
reason: body?.reason
|
|
3175
|
+
});
|
|
3176
|
+
} catch (e) {
|
|
3177
|
+
ctx.logger.warn("[MetadataPlugin] artifact reload failed", { error: e?.message });
|
|
3178
|
+
throw e;
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
});
|
|
3182
|
+
const src2 = this.options.artifactSource;
|
|
3183
|
+
const wantArtifactWatch = this.options.artifactWatch ?? src2?.mode === "local-file";
|
|
3184
|
+
if (src2?.mode === "local-file" && wantArtifactWatch && !/^https?:\/\//i.test(src2.path)) {
|
|
3185
|
+
try {
|
|
3186
|
+
const { watch: chokidarWatch2 } = await import("chokidar");
|
|
3187
|
+
const w = chokidarWatch2(src2.path, {
|
|
3188
|
+
ignoreInitial: true,
|
|
3189
|
+
awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 20 },
|
|
3190
|
+
persistent: true,
|
|
3191
|
+
// Use polling to avoid `fs.watch` exhausting the
|
|
3192
|
+
// process file-descriptor limit on macOS (chokidar
|
|
3193
|
+
// recursively wires watches on the parent
|
|
3194
|
+
// directory tree which can trip EMFILE on busy
|
|
3195
|
+
// dev hosts). 500ms polling is fast enough for
|
|
3196
|
+
// HMR (a recompile takes ~400ms anyway).
|
|
3197
|
+
usePolling: true,
|
|
3198
|
+
interval: 500,
|
|
3199
|
+
binaryInterval: 1e3
|
|
3200
|
+
});
|
|
3201
|
+
let pending = false;
|
|
3202
|
+
const reload = async () => {
|
|
3203
|
+
if (pending) return;
|
|
3204
|
+
pending = true;
|
|
3205
|
+
try {
|
|
3206
|
+
await this._loadFromLocalFile(ctx, src2.path, src2.fetchTimeoutMs);
|
|
3207
|
+
hub.broadcastReload("artifact-file-changed", [src2.path]);
|
|
3208
|
+
ctx.logger.info("[MetadataPlugin] artifact auto-reloaded (file watcher)", {
|
|
3209
|
+
path: src2.path
|
|
3210
|
+
});
|
|
3211
|
+
} catch (e) {
|
|
3212
|
+
ctx.logger.warn("[MetadataPlugin] artifact auto-reload failed", { error: e?.message });
|
|
3213
|
+
} finally {
|
|
3214
|
+
pending = false;
|
|
3215
|
+
}
|
|
3216
|
+
};
|
|
3217
|
+
w.on("change", () => {
|
|
3218
|
+
void reload();
|
|
3219
|
+
});
|
|
3220
|
+
w.on("add", () => {
|
|
3221
|
+
void reload();
|
|
3222
|
+
});
|
|
3223
|
+
this.artifactWatcher = { close: () => w.close() };
|
|
3224
|
+
console.log("[MetadataPlugin] artifact file watcher attached", src2.path);
|
|
3225
|
+
} catch (e) {
|
|
3226
|
+
ctx.logger.warn("[MetadataPlugin] artifact watcher failed to start", { error: e?.message });
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
console.log("[MetadataPlugin] HMR endpoint registered at /api/v1/dev/metadata-events");
|
|
3230
|
+
} else {
|
|
3231
|
+
console.log("[MetadataPlugin] HTTP server with getRawApp() not available \u2014 skipping HMR endpoint");
|
|
3232
|
+
}
|
|
3233
|
+
} catch (e) {
|
|
3234
|
+
console.warn("[MetadataPlugin] Failed to register HMR endpoint", e?.message);
|
|
3235
|
+
}
|
|
3236
|
+
};
|
|
3237
|
+
this.stop = async (ctx) => {
|
|
3238
|
+
if (this.artifactWatcher) {
|
|
3239
|
+
try {
|
|
3240
|
+
await this.artifactWatcher.close();
|
|
3241
|
+
} catch {
|
|
3242
|
+
}
|
|
3243
|
+
this.artifactWatcher = void 0;
|
|
3244
|
+
}
|
|
3245
|
+
try {
|
|
3246
|
+
await this.manager.dispose();
|
|
3247
|
+
} catch (e) {
|
|
3248
|
+
ctx.logger.warn("[MetadataPlugin] manager.dispose() failed", { error: e?.message });
|
|
3249
|
+
}
|
|
3250
|
+
const repo = this.repository;
|
|
3251
|
+
if (repo && typeof repo.close === "function") {
|
|
3252
|
+
try {
|
|
3253
|
+
await repo.close();
|
|
3254
|
+
} catch {
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
this.repository = void 0;
|
|
2838
3258
|
};
|
|
2839
3259
|
this.options = {
|
|
2840
3260
|
watch: true,
|
|
@@ -2912,7 +3332,12 @@ var MetadataPlugin = class {
|
|
|
2912
3332
|
const items = metadata[field];
|
|
2913
3333
|
if (!Array.isArray(items) || items.length === 0) continue;
|
|
2914
3334
|
for (const item of items) {
|
|
2915
|
-
|
|
3335
|
+
let name = item?.name;
|
|
3336
|
+
if (!name) {
|
|
3337
|
+
if (metaType === "view") {
|
|
3338
|
+
name = item?.list?.data?.object ?? item?.form?.data?.object;
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
2916
3341
|
if (!name) continue;
|
|
2917
3342
|
if (manifestPackageId && item._packageId === void 0) {
|
|
2918
3343
|
item._packageId = manifestPackageId;
|
|
@@ -3256,7 +3681,6 @@ var HistoryCleanupManager = class {
|
|
|
3256
3681
|
const driver = this.dbLoader.driver;
|
|
3257
3682
|
const historyTableName = this.dbLoader.historyTableName;
|
|
3258
3683
|
const organizationId = this.dbLoader.organizationId;
|
|
3259
|
-
const projectId = this.dbLoader.projectId;
|
|
3260
3684
|
let deleted = 0;
|
|
3261
3685
|
let errors = 0;
|
|
3262
3686
|
try {
|
|
@@ -3270,9 +3694,6 @@ var HistoryCleanupManager = class {
|
|
|
3270
3694
|
if (organizationId) {
|
|
3271
3695
|
filter.organization_id = organizationId;
|
|
3272
3696
|
}
|
|
3273
|
-
if (projectId !== void 0) {
|
|
3274
|
-
filter.project_id = projectId;
|
|
3275
|
-
}
|
|
3276
3697
|
try {
|
|
3277
3698
|
const result = await this.bulkDeleteByFilter(driver, historyTableName, filter);
|
|
3278
3699
|
deleted += result.deleted;
|
|
@@ -3285,7 +3706,6 @@ var HistoryCleanupManager = class {
|
|
|
3285
3706
|
try {
|
|
3286
3707
|
const baseWhere = {};
|
|
3287
3708
|
if (organizationId) baseWhere.organization_id = organizationId;
|
|
3288
|
-
if (projectId !== void 0) baseWhere.project_id = projectId;
|
|
3289
3709
|
const metadataIds = await driver.find(historyTableName, {
|
|
3290
3710
|
object: historyTableName,
|
|
3291
3711
|
where: baseWhere,
|
|
@@ -3373,13 +3793,11 @@ var HistoryCleanupManager = class {
|
|
|
3373
3793
|
const driver = this.dbLoader.driver;
|
|
3374
3794
|
const historyTableName = this.dbLoader.historyTableName;
|
|
3375
3795
|
const organizationId = this.dbLoader.organizationId;
|
|
3376
|
-
const projectId = this.dbLoader.projectId;
|
|
3377
3796
|
let recordsByAge = 0;
|
|
3378
3797
|
let recordsByCount = 0;
|
|
3379
3798
|
try {
|
|
3380
3799
|
const baseWhere = {};
|
|
3381
3800
|
if (organizationId) baseWhere.organization_id = organizationId;
|
|
3382
|
-
if (projectId !== void 0) baseWhere.project_id = projectId;
|
|
3383
3801
|
if (this.policy.maxAgeDays) {
|
|
3384
3802
|
const cutoffDate = /* @__PURE__ */ new Date();
|
|
3385
3803
|
cutoffDate.setDate(cutoffDate.getDate() - this.policy.maxAgeDays);
|