@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/index.cjs +292 -64
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +77 -5
- package/dist/index.d.ts +77 -5
- package/dist/index.js +292 -64
- package/dist/index.js.map +1 -1
- package/dist/node.cjs +292 -64
- 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 +292 -64
- package/dist/node.js.map +1 -1
- package/package.json +7 -5
package/dist/index.cjs
CHANGED
|
@@ -64,7 +64,12 @@ function registerMetadataHmrRoutes(app, manager, options = {}) {
|
|
|
64
64
|
metadataType: evt.metadataType ?? type,
|
|
65
65
|
name: evt.name ?? "",
|
|
66
66
|
path: evt.path,
|
|
67
|
-
timestamp: Number.isFinite(ts) ? ts : Date.now()
|
|
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 } : {}
|
|
68
73
|
});
|
|
69
74
|
});
|
|
70
75
|
}
|
|
@@ -603,7 +608,7 @@ var DatabaseLoader = class {
|
|
|
603
608
|
this.tableName = options.tableName ?? "sys_metadata";
|
|
604
609
|
this.historyTableName = options.historyTableName ?? "sys_metadata_history";
|
|
605
610
|
this.organizationId = options.organizationId;
|
|
606
|
-
|
|
611
|
+
void options.projectId;
|
|
607
612
|
this.trackHistory = options.trackHistory !== false;
|
|
608
613
|
const cacheOpts = options.cache;
|
|
609
614
|
const cacheEnabled = cacheOpts?.enabled !== false;
|
|
@@ -693,6 +698,26 @@ var DatabaseLoader = class {
|
|
|
693
698
|
}
|
|
694
699
|
return this.driver.delete(table, id);
|
|
695
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
|
+
}
|
|
696
721
|
/**
|
|
697
722
|
* Ensure the metadata table exists.
|
|
698
723
|
* Uses IDataDriver.syncSchema with the SysMetadataObject definition
|
|
@@ -757,8 +782,9 @@ var DatabaseLoader = class {
|
|
|
757
782
|
}
|
|
758
783
|
/**
|
|
759
784
|
* Build base filter conditions for queries.
|
|
760
|
-
* Filters by organizationId when configured
|
|
761
|
-
*
|
|
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).
|
|
762
788
|
*/
|
|
763
789
|
baseFilter(type, name) {
|
|
764
790
|
const filter = { type };
|
|
@@ -768,13 +794,11 @@ var DatabaseLoader = class {
|
|
|
768
794
|
if (this.organizationId) {
|
|
769
795
|
filter.organization_id = this.organizationId;
|
|
770
796
|
}
|
|
771
|
-
filter.project_id = this.projectId ?? null;
|
|
772
797
|
return filter;
|
|
773
798
|
}
|
|
774
799
|
/**
|
|
775
800
|
* Create a history record for a metadata change.
|
|
776
801
|
*
|
|
777
|
-
* @param metadataId - The metadata record ID
|
|
778
802
|
* @param type - Metadata type
|
|
779
803
|
* @param name - Metadata name
|
|
780
804
|
* @param version - Version number
|
|
@@ -784,7 +808,7 @@ var DatabaseLoader = class {
|
|
|
784
808
|
* @param changeNote - Optional change description
|
|
785
809
|
* @param recordedBy - Optional user who made the change
|
|
786
810
|
*/
|
|
787
|
-
async createHistoryRecord(
|
|
811
|
+
async createHistoryRecord(type, name, version, metadata, operationType, previousChecksum, changeNote, recordedBy) {
|
|
788
812
|
if (!this.trackHistory) return;
|
|
789
813
|
await this.ensureHistorySchema();
|
|
790
814
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -794,9 +818,9 @@ var DatabaseLoader = class {
|
|
|
794
818
|
}
|
|
795
819
|
const historyId = generateId();
|
|
796
820
|
const metadataJson = JSON.stringify(metadata);
|
|
821
|
+
const eventSeq = await this.nextEventSeq();
|
|
797
822
|
const historyRecord = {
|
|
798
823
|
id: historyId,
|
|
799
|
-
metadataId,
|
|
800
824
|
name,
|
|
801
825
|
type,
|
|
802
826
|
version,
|
|
@@ -807,13 +831,12 @@ var DatabaseLoader = class {
|
|
|
807
831
|
changeNote,
|
|
808
832
|
recordedBy,
|
|
809
833
|
recordedAt: now,
|
|
810
|
-
...this.organizationId ? { organizationId: this.organizationId } : {}
|
|
811
|
-
...this.projectId !== void 0 ? { projectId: this.projectId } : {}
|
|
834
|
+
...this.organizationId ? { organizationId: this.organizationId } : {}
|
|
812
835
|
};
|
|
813
836
|
try {
|
|
814
837
|
await this._create(this.historyTableName, {
|
|
815
838
|
id: historyRecord.id,
|
|
816
|
-
|
|
839
|
+
event_seq: eventSeq,
|
|
817
840
|
name: historyRecord.name,
|
|
818
841
|
type: historyRecord.type,
|
|
819
842
|
version: historyRecord.version,
|
|
@@ -824,8 +847,8 @@ var DatabaseLoader = class {
|
|
|
824
847
|
change_note: historyRecord.changeNote,
|
|
825
848
|
recorded_by: historyRecord.recordedBy,
|
|
826
849
|
recorded_at: historyRecord.recordedAt,
|
|
827
|
-
|
|
828
|
-
...this.
|
|
850
|
+
source: "database-loader",
|
|
851
|
+
...this.organizationId ? { organization_id: this.organizationId } : {}
|
|
829
852
|
});
|
|
830
853
|
} catch (error) {
|
|
831
854
|
console.error(`Failed to create history record for ${type}/${name}:`, error);
|
|
@@ -1001,25 +1024,20 @@ var DatabaseLoader = class {
|
|
|
1001
1024
|
async getHistoryRecord(type, name, version) {
|
|
1002
1025
|
if (!this.trackHistory) return null;
|
|
1003
1026
|
await this.ensureHistorySchema();
|
|
1004
|
-
const metadataRow = await this._findOne(this.tableName, {
|
|
1005
|
-
where: this.baseFilter(type, name)
|
|
1006
|
-
});
|
|
1007
|
-
if (!metadataRow) return null;
|
|
1008
1027
|
const filter = {
|
|
1009
|
-
|
|
1028
|
+
type,
|
|
1029
|
+
name,
|
|
1010
1030
|
version
|
|
1011
1031
|
};
|
|
1012
1032
|
if (this.organizationId) {
|
|
1013
1033
|
filter.organization_id = this.organizationId;
|
|
1014
1034
|
}
|
|
1015
|
-
filter.project_id = this.projectId ?? null;
|
|
1016
1035
|
const row = await this._findOne(this.historyTableName, {
|
|
1017
1036
|
where: filter
|
|
1018
1037
|
});
|
|
1019
1038
|
if (!row) return null;
|
|
1020
1039
|
return {
|
|
1021
1040
|
id: row.id,
|
|
1022
|
-
metadataId: row.metadata_id,
|
|
1023
1041
|
name: row.name,
|
|
1024
1042
|
type: row.type,
|
|
1025
1043
|
version: row.version,
|
|
@@ -1029,7 +1047,6 @@ var DatabaseLoader = class {
|
|
|
1029
1047
|
previousChecksum: row.previous_checksum,
|
|
1030
1048
|
changeNote: row.change_note,
|
|
1031
1049
|
organizationId: row.organization_id,
|
|
1032
|
-
projectId: row.project_id,
|
|
1033
1050
|
recordedBy: row.recorded_by,
|
|
1034
1051
|
recordedAt: row.recorded_at
|
|
1035
1052
|
};
|
|
@@ -1045,18 +1062,11 @@ var DatabaseLoader = class {
|
|
|
1045
1062
|
}
|
|
1046
1063
|
await this.ensureSchema();
|
|
1047
1064
|
await this.ensureHistorySchema();
|
|
1048
|
-
const filter = { type, name };
|
|
1049
|
-
if (this.organizationId) filter.organization_id = this.organizationId;
|
|
1050
|
-
filter.project_id = this.projectId ?? null;
|
|
1051
|
-
const metadataRecord = await this._findOne(this.tableName, { where: filter });
|
|
1052
|
-
if (!metadataRecord) {
|
|
1053
|
-
return { records: [], total: 0, hasMore: false };
|
|
1054
|
-
}
|
|
1055
1065
|
const historyFilter = {
|
|
1056
|
-
|
|
1066
|
+
type,
|
|
1067
|
+
name
|
|
1057
1068
|
};
|
|
1058
1069
|
if (this.organizationId) historyFilter.organization_id = this.organizationId;
|
|
1059
|
-
historyFilter.project_id = this.projectId ?? null;
|
|
1060
1070
|
if (options?.operationType) historyFilter.operation_type = options.operationType;
|
|
1061
1071
|
if (options?.since) historyFilter.recorded_at = { $gte: options.since };
|
|
1062
1072
|
if (options?.until) {
|
|
@@ -1085,7 +1095,6 @@ var DatabaseLoader = class {
|
|
|
1085
1095
|
const parsedMetadata = typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata;
|
|
1086
1096
|
return {
|
|
1087
1097
|
id: row.id,
|
|
1088
|
-
metadataId: row.metadata_id,
|
|
1089
1098
|
name: row.name,
|
|
1090
1099
|
type: row.type,
|
|
1091
1100
|
version: row.version,
|
|
@@ -1095,7 +1104,6 @@ var DatabaseLoader = class {
|
|
|
1095
1104
|
previousChecksum: row.previous_checksum,
|
|
1096
1105
|
changeNote: row.change_note,
|
|
1097
1106
|
organizationId: row.organization_id,
|
|
1098
|
-
projectId: row.project_id,
|
|
1099
1107
|
recordedBy: row.recorded_by,
|
|
1100
1108
|
recordedAt: row.recorded_at
|
|
1101
1109
|
};
|
|
@@ -1131,7 +1139,6 @@ var DatabaseLoader = class {
|
|
|
1131
1139
|
});
|
|
1132
1140
|
this.invalidate(type, name);
|
|
1133
1141
|
await this.createHistoryRecord(
|
|
1134
|
-
existing.id,
|
|
1135
1142
|
type,
|
|
1136
1143
|
name,
|
|
1137
1144
|
newVersion,
|
|
@@ -1173,7 +1180,6 @@ var DatabaseLoader = class {
|
|
|
1173
1180
|
});
|
|
1174
1181
|
this.invalidate(type, name);
|
|
1175
1182
|
await this.createHistoryRecord(
|
|
1176
|
-
existing.id,
|
|
1177
1183
|
type,
|
|
1178
1184
|
name,
|
|
1179
1185
|
version,
|
|
@@ -1202,13 +1208,11 @@ var DatabaseLoader = class {
|
|
|
1202
1208
|
version: 1,
|
|
1203
1209
|
source: "database",
|
|
1204
1210
|
...this.organizationId ? { organization_id: this.organizationId } : {},
|
|
1205
|
-
...this.projectId !== void 0 ? { project_id: this.projectId } : { project_id: null },
|
|
1206
1211
|
created_at: now,
|
|
1207
1212
|
updated_at: now
|
|
1208
1213
|
});
|
|
1209
1214
|
this.invalidate(type, name);
|
|
1210
1215
|
await this.createHistoryRecord(
|
|
1211
|
-
id,
|
|
1212
1216
|
type,
|
|
1213
1217
|
name,
|
|
1214
1218
|
1,
|
|
@@ -1275,6 +1279,7 @@ var _MetadataManager = class _MetadataManager {
|
|
|
1275
1279
|
// Invalidated on every `register()` / `unregister()` to keep CRUD writes
|
|
1276
1280
|
// visible to subsequent reads.
|
|
1277
1281
|
this.listCache = /* @__PURE__ */ new Map();
|
|
1282
|
+
this.repoWatchClosed = false;
|
|
1278
1283
|
this.config = config;
|
|
1279
1284
|
this.logger = (0, import_core.createLogger)({ level: "info", format: "pretty" });
|
|
1280
1285
|
this.serializers = /* @__PURE__ */ new Map();
|
|
@@ -2282,6 +2287,110 @@ var _MetadataManager = class _MetadataManager {
|
|
|
2282
2287
|
*/
|
|
2283
2288
|
async stopWatching() {
|
|
2284
2289
|
}
|
|
2290
|
+
// ─── ADR-0008 PR-6: Repository wiring ───────────────────────────────
|
|
2291
|
+
/**
|
|
2292
|
+
* Attach a {@link MetadataRepository} as a supplementary event source.
|
|
2293
|
+
*
|
|
2294
|
+
* The manager subscribes to `repo.watch({})` and re-emits each event
|
|
2295
|
+
* through {@link notifyWatchers} as a legacy `MetadataWatchEvent`.
|
|
2296
|
+
* Each event also invalidates the in-memory registry entry and the
|
|
2297
|
+
* `list()` cache for the affected type so subsequent reads see fresh
|
|
2298
|
+
* data.
|
|
2299
|
+
*
|
|
2300
|
+
* No write-through. `register()` / `unregister()` / `save()` are
|
|
2301
|
+
* untouched in this PR (deferred to ADR-0008 M0 PR-10).
|
|
2302
|
+
*
|
|
2303
|
+
* Call {@link dispose} (or {@link stopRepositoryWatch}) to detach.
|
|
2304
|
+
*/
|
|
2305
|
+
setRepository(repo) {
|
|
2306
|
+
if (this.repository === repo) return;
|
|
2307
|
+
if (this.repository) {
|
|
2308
|
+
void this.stopRepositoryWatch();
|
|
2309
|
+
}
|
|
2310
|
+
this.repository = repo;
|
|
2311
|
+
this.repoWatchClosed = false;
|
|
2312
|
+
void this.startRepositoryWatch();
|
|
2313
|
+
}
|
|
2314
|
+
/** Return the attached repository, if any. */
|
|
2315
|
+
getRepository() {
|
|
2316
|
+
return this.repository;
|
|
2317
|
+
}
|
|
2318
|
+
/** Stop the active repo.watch() loop (best-effort). */
|
|
2319
|
+
async stopRepositoryWatch() {
|
|
2320
|
+
this.repoWatchClosed = true;
|
|
2321
|
+
const iter = this.repoWatchIter;
|
|
2322
|
+
this.repoWatchIter = void 0;
|
|
2323
|
+
if (iter && typeof iter.return === "function") {
|
|
2324
|
+
try {
|
|
2325
|
+
await iter.return(void 0);
|
|
2326
|
+
} catch {
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
/**
|
|
2331
|
+
* Best-effort cleanup. Stops the FS watcher (if any), drains the
|
|
2332
|
+
* repository watch loop, and clears registry caches. Safe to call
|
|
2333
|
+
* multiple times.
|
|
2334
|
+
*/
|
|
2335
|
+
async dispose() {
|
|
2336
|
+
await this.stopWatching().catch(() => void 0);
|
|
2337
|
+
await this.stopRepositoryWatch().catch(() => void 0);
|
|
2338
|
+
this.listCache.clear();
|
|
2339
|
+
}
|
|
2340
|
+
async startRepositoryWatch() {
|
|
2341
|
+
const repo = this.repository;
|
|
2342
|
+
if (!repo) return;
|
|
2343
|
+
const iterable = repo.watch({});
|
|
2344
|
+
const iter = iterable[Symbol.asyncIterator]();
|
|
2345
|
+
this.repoWatchIter = iter;
|
|
2346
|
+
try {
|
|
2347
|
+
while (!this.repoWatchClosed) {
|
|
2348
|
+
const { value, done } = await iter.next();
|
|
2349
|
+
if (done) break;
|
|
2350
|
+
try {
|
|
2351
|
+
this.applyRepoEvent(value);
|
|
2352
|
+
} catch (err) {
|
|
2353
|
+
this.logger.warn("[MetadataManager] repo event handler failed", {
|
|
2354
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
} catch (err) {
|
|
2359
|
+
if (!this.repoWatchClosed) {
|
|
2360
|
+
this.logger.warn("[MetadataManager] repository watch loop exited unexpectedly", {
|
|
2361
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2362
|
+
});
|
|
2363
|
+
}
|
|
2364
|
+
} finally {
|
|
2365
|
+
if (this.repoWatchIter === iter) this.repoWatchIter = void 0;
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
/** Translate a repo event to the legacy MetadataWatchEvent + invalidate caches. */
|
|
2369
|
+
applyRepoEvent(evt) {
|
|
2370
|
+
const ref = evt.ref;
|
|
2371
|
+
const type = ref.type;
|
|
2372
|
+
const name = ref.name;
|
|
2373
|
+
const typeStore = this.registry.get(type);
|
|
2374
|
+
if (typeStore) {
|
|
2375
|
+
typeStore.delete(name);
|
|
2376
|
+
if (typeStore.size === 0) this.registry.delete(type);
|
|
2377
|
+
}
|
|
2378
|
+
this.listCache.delete(type);
|
|
2379
|
+
const legacyType = evt.op === "create" ? "added" : evt.op === "delete" ? "deleted" : "changed";
|
|
2380
|
+
const legacyEvent = {
|
|
2381
|
+
type: legacyType,
|
|
2382
|
+
metadataType: type,
|
|
2383
|
+
name,
|
|
2384
|
+
path: "",
|
|
2385
|
+
// Repo events carry the hash only; the body is fetched on demand
|
|
2386
|
+
// via manager.get(type, name). HMR consumers don't read `data` so
|
|
2387
|
+
// this is fine for M0. (See ADR-0008 §12 open question 1.)
|
|
2388
|
+
data: void 0,
|
|
2389
|
+
timestamp: evt.ts
|
|
2390
|
+
};
|
|
2391
|
+
legacyEvent.seq = evt.seq;
|
|
2392
|
+
this.notifyWatchers(type, legacyEvent);
|
|
2393
|
+
}
|
|
2285
2394
|
notifyWatchers(type, event) {
|
|
2286
2395
|
const callbacks = this.watchCallbacks.get(type);
|
|
2287
2396
|
if (!callbacks) return;
|
|
@@ -2757,7 +2866,13 @@ var NodeMetadataManager = class extends MetadataManager {
|
|
|
2757
2866
|
this.watcher = (0, import_chokidar.watch)(rootDir, {
|
|
2758
2867
|
ignored,
|
|
2759
2868
|
persistent,
|
|
2760
|
-
ignoreInitial: true
|
|
2869
|
+
ignoreInitial: true,
|
|
2870
|
+
// Use polling to avoid `fs.watch` EMFILE on macOS / busy dev hosts.
|
|
2871
|
+
// Recursive watch over a project root would otherwise wire native
|
|
2872
|
+
// watches across the entire tree, easily exhausting the FD pool.
|
|
2873
|
+
usePolling: true,
|
|
2874
|
+
interval: 1e3,
|
|
2875
|
+
binaryInterval: 2e3
|
|
2761
2876
|
});
|
|
2762
2877
|
this.watcher.on("add", async (filePath) => {
|
|
2763
2878
|
await this.handleFileEvent("added", filePath);
|
|
@@ -2892,6 +3007,7 @@ var queryableMetadataObjects = [
|
|
|
2892
3007
|
import_metadata2.SysMetadataObject,
|
|
2893
3008
|
import_metadata2.SysMetadataHistoryObject
|
|
2894
3009
|
];
|
|
3010
|
+
var REPO_SUBDIR = ".objectstack/metadata";
|
|
2895
3011
|
var ARTIFACT_FIELD_TO_TYPE = {
|
|
2896
3012
|
objects: "object",
|
|
2897
3013
|
objectExtensions: "object_extension",
|
|
@@ -2989,6 +3105,31 @@ var MetadataPlugin = class {
|
|
|
2989
3105
|
await this._loadFromFileSystem(ctx);
|
|
2990
3106
|
}
|
|
2991
3107
|
}
|
|
3108
|
+
const bootstrapMode = this.options.config?.bootstrap ?? "eager";
|
|
3109
|
+
if (bootstrapMode !== "artifact-only") {
|
|
3110
|
+
try {
|
|
3111
|
+
const path3 = await import("path");
|
|
3112
|
+
const { FileSystemRepository } = await import("@objectstack/metadata-fs");
|
|
3113
|
+
const rootDir = this.options.rootDir || process.cwd();
|
|
3114
|
+
const repoRoot = path3.join(rootDir, REPO_SUBDIR);
|
|
3115
|
+
const repo = new FileSystemRepository({
|
|
3116
|
+
root: repoRoot,
|
|
3117
|
+
org: this.options.organizationId ?? "system",
|
|
3118
|
+
disableWatch: this.options.watch === false
|
|
3119
|
+
});
|
|
3120
|
+
await repo.start();
|
|
3121
|
+
this.repository = repo;
|
|
3122
|
+
this.manager.setRepository(repo);
|
|
3123
|
+
ctx.logger.info("[MetadataPlugin] FileSystemRepository attached", {
|
|
3124
|
+
repoRoot,
|
|
3125
|
+
watch: this.options.watch !== false
|
|
3126
|
+
});
|
|
3127
|
+
} catch (e) {
|
|
3128
|
+
ctx.logger.warn("[MetadataPlugin] Failed to attach FileSystemRepository", {
|
|
3129
|
+
error: e?.message
|
|
3130
|
+
});
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
2992
3133
|
try {
|
|
2993
3134
|
const realtimeService = ctx.getService("realtime");
|
|
2994
3135
|
if (realtimeService && typeof realtimeService === "object" && "publish" in realtimeService) {
|
|
@@ -3006,12 +3147,12 @@ var MetadataPlugin = class {
|
|
|
3006
3147
|
const { registerMetadataHmrRoutes: registerMetadataHmrRoutes2 } = await Promise.resolve().then(() => (init_hmr_routes(), hmr_routes_exports));
|
|
3007
3148
|
const hub = registerMetadataHmrRoutes2(httpServer.getRawApp(), this.manager);
|
|
3008
3149
|
hub.setOnPostReload(async (body = {}) => {
|
|
3009
|
-
const
|
|
3010
|
-
if (
|
|
3150
|
+
const src3 = this.options.artifactSource;
|
|
3151
|
+
if (src3?.mode === "local-file") {
|
|
3011
3152
|
try {
|
|
3012
|
-
await this._loadFromLocalFile(ctx,
|
|
3153
|
+
await this._loadFromLocalFile(ctx, src3.path, src3.fetchTimeoutMs);
|
|
3013
3154
|
ctx.logger.info("[MetadataPlugin] artifact reloaded via HMR POST", {
|
|
3014
|
-
path:
|
|
3155
|
+
path: src3.path,
|
|
3015
3156
|
reason: body?.reason
|
|
3016
3157
|
});
|
|
3017
3158
|
} catch (e) {
|
|
@@ -3020,6 +3161,53 @@ var MetadataPlugin = class {
|
|
|
3020
3161
|
}
|
|
3021
3162
|
}
|
|
3022
3163
|
});
|
|
3164
|
+
const src2 = this.options.artifactSource;
|
|
3165
|
+
const wantArtifactWatch = this.options.artifactWatch ?? src2?.mode === "local-file";
|
|
3166
|
+
if (src2?.mode === "local-file" && wantArtifactWatch && !/^https?:\/\//i.test(src2.path)) {
|
|
3167
|
+
try {
|
|
3168
|
+
const { watch: chokidarWatch2 } = await import("chokidar");
|
|
3169
|
+
const w = chokidarWatch2(src2.path, {
|
|
3170
|
+
ignoreInitial: true,
|
|
3171
|
+
awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 20 },
|
|
3172
|
+
persistent: true,
|
|
3173
|
+
// Use polling to avoid `fs.watch` exhausting the
|
|
3174
|
+
// process file-descriptor limit on macOS (chokidar
|
|
3175
|
+
// recursively wires watches on the parent
|
|
3176
|
+
// directory tree which can trip EMFILE on busy
|
|
3177
|
+
// dev hosts). 500ms polling is fast enough for
|
|
3178
|
+
// HMR (a recompile takes ~400ms anyway).
|
|
3179
|
+
usePolling: true,
|
|
3180
|
+
interval: 500,
|
|
3181
|
+
binaryInterval: 1e3
|
|
3182
|
+
});
|
|
3183
|
+
let pending = false;
|
|
3184
|
+
const reload = async () => {
|
|
3185
|
+
if (pending) return;
|
|
3186
|
+
pending = true;
|
|
3187
|
+
try {
|
|
3188
|
+
await this._loadFromLocalFile(ctx, src2.path, src2.fetchTimeoutMs);
|
|
3189
|
+
hub.broadcastReload("artifact-file-changed", [src2.path]);
|
|
3190
|
+
ctx.logger.info("[MetadataPlugin] artifact auto-reloaded (file watcher)", {
|
|
3191
|
+
path: src2.path
|
|
3192
|
+
});
|
|
3193
|
+
} catch (e) {
|
|
3194
|
+
ctx.logger.warn("[MetadataPlugin] artifact auto-reload failed", { error: e?.message });
|
|
3195
|
+
} finally {
|
|
3196
|
+
pending = false;
|
|
3197
|
+
}
|
|
3198
|
+
};
|
|
3199
|
+
w.on("change", () => {
|
|
3200
|
+
void reload();
|
|
3201
|
+
});
|
|
3202
|
+
w.on("add", () => {
|
|
3203
|
+
void reload();
|
|
3204
|
+
});
|
|
3205
|
+
this.artifactWatcher = { close: () => w.close() };
|
|
3206
|
+
console.log("[MetadataPlugin] artifact file watcher attached", src2.path);
|
|
3207
|
+
} catch (e) {
|
|
3208
|
+
ctx.logger.warn("[MetadataPlugin] artifact watcher failed to start", { error: e?.message });
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3023
3211
|
console.log("[MetadataPlugin] HMR endpoint registered at /api/v1/dev/metadata-events");
|
|
3024
3212
|
} else {
|
|
3025
3213
|
console.log("[MetadataPlugin] HTTP server with getRawApp() not available \u2014 skipping HMR endpoint");
|
|
@@ -3028,6 +3216,28 @@ var MetadataPlugin = class {
|
|
|
3028
3216
|
console.warn("[MetadataPlugin] Failed to register HMR endpoint", e?.message);
|
|
3029
3217
|
}
|
|
3030
3218
|
};
|
|
3219
|
+
this.stop = async (ctx) => {
|
|
3220
|
+
if (this.artifactWatcher) {
|
|
3221
|
+
try {
|
|
3222
|
+
await this.artifactWatcher.close();
|
|
3223
|
+
} catch {
|
|
3224
|
+
}
|
|
3225
|
+
this.artifactWatcher = void 0;
|
|
3226
|
+
}
|
|
3227
|
+
try {
|
|
3228
|
+
await this.manager.dispose();
|
|
3229
|
+
} catch (e) {
|
|
3230
|
+
ctx.logger.warn("[MetadataPlugin] manager.dispose() failed", { error: e?.message });
|
|
3231
|
+
}
|
|
3232
|
+
const repo = this.repository;
|
|
3233
|
+
if (repo && typeof repo.close === "function") {
|
|
3234
|
+
try {
|
|
3235
|
+
await repo.close();
|
|
3236
|
+
} catch {
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
this.repository = void 0;
|
|
3240
|
+
};
|
|
3031
3241
|
this.options = {
|
|
3032
3242
|
watch: true,
|
|
3033
3243
|
...options
|
|
@@ -3104,7 +3314,12 @@ var MetadataPlugin = class {
|
|
|
3104
3314
|
const items = metadata[field];
|
|
3105
3315
|
if (!Array.isArray(items) || items.length === 0) continue;
|
|
3106
3316
|
for (const item of items) {
|
|
3107
|
-
|
|
3317
|
+
let name = item?.name;
|
|
3318
|
+
if (!name) {
|
|
3319
|
+
if (metaType === "view") {
|
|
3320
|
+
name = item?.list?.data?.object ?? item?.form?.data?.object;
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3108
3323
|
if (!name) continue;
|
|
3109
3324
|
if (manifestPackageId && item._packageId === void 0) {
|
|
3110
3325
|
item._packageId = manifestPackageId;
|
|
@@ -3413,6 +3628,10 @@ function registerMetadataHistoryRoutes(app, metadataService) {
|
|
|
3413
3628
|
}
|
|
3414
3629
|
|
|
3415
3630
|
// src/utils/history-cleanup.ts
|
|
3631
|
+
var import_kernel2 = require("@objectstack/spec/kernel");
|
|
3632
|
+
function executionPinnedTypes() {
|
|
3633
|
+
return import_kernel2.DEFAULT_METADATA_TYPE_REGISTRY.filter((entry) => entry.executionPinned).map((entry) => entry.type);
|
|
3634
|
+
}
|
|
3416
3635
|
var HistoryCleanupManager = class {
|
|
3417
3636
|
constructor(policy, dbLoader) {
|
|
3418
3637
|
this.policy = policy;
|
|
@@ -3448,9 +3667,10 @@ var HistoryCleanupManager = class {
|
|
|
3448
3667
|
const driver = this.dbLoader.driver;
|
|
3449
3668
|
const historyTableName = this.dbLoader.historyTableName;
|
|
3450
3669
|
const organizationId = this.dbLoader.organizationId;
|
|
3451
|
-
const projectId = this.dbLoader.projectId;
|
|
3452
3670
|
let deleted = 0;
|
|
3453
3671
|
let errors = 0;
|
|
3672
|
+
const pinnedTypes = executionPinnedTypes();
|
|
3673
|
+
const isPinned = (t) => !!t && pinnedTypes.includes(t);
|
|
3454
3674
|
try {
|
|
3455
3675
|
if (this.policy.maxAgeDays) {
|
|
3456
3676
|
const cutoffDate = /* @__PURE__ */ new Date();
|
|
@@ -3462,8 +3682,8 @@ var HistoryCleanupManager = class {
|
|
|
3462
3682
|
if (organizationId) {
|
|
3463
3683
|
filter.organization_id = organizationId;
|
|
3464
3684
|
}
|
|
3465
|
-
if (
|
|
3466
|
-
filter.
|
|
3685
|
+
if (pinnedTypes.length > 0) {
|
|
3686
|
+
filter.type = { $nin: pinnedTypes };
|
|
3467
3687
|
}
|
|
3468
3688
|
try {
|
|
3469
3689
|
const result = await this.bulkDeleteByFilter(driver, historyTableName, filter);
|
|
@@ -3477,20 +3697,22 @@ var HistoryCleanupManager = class {
|
|
|
3477
3697
|
try {
|
|
3478
3698
|
const baseWhere = {};
|
|
3479
3699
|
if (organizationId) baseWhere.organization_id = organizationId;
|
|
3480
|
-
|
|
3481
|
-
const metadataIds = await driver.find(historyTableName, {
|
|
3700
|
+
const metaItems = await driver.find(historyTableName, {
|
|
3482
3701
|
object: historyTableName,
|
|
3483
3702
|
where: baseWhere,
|
|
3484
|
-
fields: ["
|
|
3703
|
+
fields: ["type", "name"]
|
|
3485
3704
|
});
|
|
3486
|
-
const
|
|
3487
|
-
for (const record of
|
|
3488
|
-
|
|
3489
|
-
|
|
3705
|
+
const uniqueKeys = /* @__PURE__ */ new Set();
|
|
3706
|
+
for (const record of metaItems) {
|
|
3707
|
+
const t = record.type;
|
|
3708
|
+
const n = record.name;
|
|
3709
|
+
if (t && n && !isPinned(t)) {
|
|
3710
|
+
uniqueKeys.add(`${t}${n}`);
|
|
3490
3711
|
}
|
|
3491
3712
|
}
|
|
3492
|
-
for (const
|
|
3493
|
-
const
|
|
3713
|
+
for (const key of uniqueKeys) {
|
|
3714
|
+
const [type, name] = key.split("");
|
|
3715
|
+
const filter = { type, name, ...baseWhere };
|
|
3494
3716
|
try {
|
|
3495
3717
|
const historyRecords = await driver.find(historyTableName, {
|
|
3496
3718
|
object: historyTableName,
|
|
@@ -3565,13 +3787,13 @@ var HistoryCleanupManager = class {
|
|
|
3565
3787
|
const driver = this.dbLoader.driver;
|
|
3566
3788
|
const historyTableName = this.dbLoader.historyTableName;
|
|
3567
3789
|
const organizationId = this.dbLoader.organizationId;
|
|
3568
|
-
const projectId = this.dbLoader.projectId;
|
|
3569
3790
|
let recordsByAge = 0;
|
|
3570
3791
|
let recordsByCount = 0;
|
|
3792
|
+
const pinnedTypes = executionPinnedTypes();
|
|
3793
|
+
const isPinned = (t) => !!t && pinnedTypes.includes(t);
|
|
3571
3794
|
try {
|
|
3572
3795
|
const baseWhere = {};
|
|
3573
3796
|
if (organizationId) baseWhere.organization_id = organizationId;
|
|
3574
|
-
if (projectId !== void 0) baseWhere.project_id = projectId;
|
|
3575
3797
|
if (this.policy.maxAgeDays) {
|
|
3576
3798
|
const cutoffDate = /* @__PURE__ */ new Date();
|
|
3577
3799
|
cutoffDate.setDate(cutoffDate.getDate() - this.policy.maxAgeDays);
|
|
@@ -3580,25 +3802,31 @@ var HistoryCleanupManager = class {
|
|
|
3580
3802
|
recorded_at: { $lt: cutoffISO },
|
|
3581
3803
|
...baseWhere
|
|
3582
3804
|
};
|
|
3805
|
+
if (pinnedTypes.length > 0) {
|
|
3806
|
+
filter.type = { $nin: pinnedTypes };
|
|
3807
|
+
}
|
|
3583
3808
|
recordsByAge = await driver.count(historyTableName, {
|
|
3584
3809
|
object: historyTableName,
|
|
3585
3810
|
where: filter
|
|
3586
3811
|
});
|
|
3587
3812
|
}
|
|
3588
3813
|
if (this.policy.maxVersions) {
|
|
3589
|
-
const
|
|
3814
|
+
const metaItems = await driver.find(historyTableName, {
|
|
3590
3815
|
object: historyTableName,
|
|
3591
3816
|
where: baseWhere,
|
|
3592
|
-
fields: ["
|
|
3817
|
+
fields: ["type", "name"]
|
|
3593
3818
|
});
|
|
3594
|
-
const
|
|
3595
|
-
for (const record of
|
|
3596
|
-
|
|
3597
|
-
|
|
3819
|
+
const uniqueKeys = /* @__PURE__ */ new Set();
|
|
3820
|
+
for (const record of metaItems) {
|
|
3821
|
+
const t = record.type;
|
|
3822
|
+
const n = record.name;
|
|
3823
|
+
if (t && n && !isPinned(t)) {
|
|
3824
|
+
uniqueKeys.add(`${t}${n}`);
|
|
3598
3825
|
}
|
|
3599
3826
|
}
|
|
3600
|
-
for (const
|
|
3601
|
-
const
|
|
3827
|
+
for (const key of uniqueKeys) {
|
|
3828
|
+
const [type, name] = key.split("");
|
|
3829
|
+
const filter = { type, name, ...baseWhere };
|
|
3602
3830
|
const count = await driver.count(historyTableName, {
|
|
3603
3831
|
object: historyTableName,
|
|
3604
3832
|
where: filter
|