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