@objectstack/metadata 4.0.0 → 4.0.2
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 +967 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2604 -33
- package/dist/index.d.ts +2604 -33
- package/dist/index.js +960 -17
- package/dist/index.js.map +1 -1
- package/dist/node.cjs +967 -18
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +2 -2
- package/dist/node.d.ts +2 -2
- package/dist/node.js +960 -17
- package/dist/node.js.map +1 -1
- package/package.json +5 -5
package/dist/node.cjs
CHANGED
|
@@ -32,6 +32,7 @@ var node_exports = {};
|
|
|
32
32
|
__export(node_exports, {
|
|
33
33
|
DatabaseLoader: () => DatabaseLoader,
|
|
34
34
|
FilesystemLoader: () => FilesystemLoader,
|
|
35
|
+
HistoryCleanupManager: () => HistoryCleanupManager,
|
|
35
36
|
JSONSerializer: () => JSONSerializer,
|
|
36
37
|
MemoryLoader: () => MemoryLoader,
|
|
37
38
|
MetadataManager: () => MetadataManager,
|
|
@@ -39,9 +40,14 @@ __export(node_exports, {
|
|
|
39
40
|
Migration: () => migration_exports,
|
|
40
41
|
NodeMetadataManager: () => NodeMetadataManager,
|
|
41
42
|
RemoteLoader: () => RemoteLoader,
|
|
43
|
+
SysMetadataHistoryObject: () => SysMetadataHistoryObject,
|
|
42
44
|
SysMetadataObject: () => SysMetadataObject,
|
|
43
45
|
TypeScriptSerializer: () => TypeScriptSerializer,
|
|
44
|
-
YAMLSerializer: () => YAMLSerializer
|
|
46
|
+
YAMLSerializer: () => YAMLSerializer,
|
|
47
|
+
calculateChecksum: () => calculateChecksum,
|
|
48
|
+
generateDiffSummary: () => generateDiffSummary,
|
|
49
|
+
generateSimpleDiff: () => generateSimpleDiff,
|
|
50
|
+
registerMetadataHistoryRoutes: () => registerMetadataHistoryRoutes
|
|
45
51
|
});
|
|
46
52
|
module.exports = __toCommonJS(node_exports);
|
|
47
53
|
|
|
@@ -367,6 +373,224 @@ var SysMetadataObject = import_data.ObjectSchema.create({
|
|
|
367
373
|
}
|
|
368
374
|
});
|
|
369
375
|
|
|
376
|
+
// src/objects/sys-metadata-history.object.ts
|
|
377
|
+
var import_data2 = require("@objectstack/spec/data");
|
|
378
|
+
var SysMetadataHistoryObject = import_data2.ObjectSchema.create({
|
|
379
|
+
namespace: "sys",
|
|
380
|
+
name: "metadata_history",
|
|
381
|
+
label: "Metadata History",
|
|
382
|
+
pluralLabel: "Metadata History",
|
|
383
|
+
icon: "history",
|
|
384
|
+
isSystem: true,
|
|
385
|
+
description: "Version history and audit trail for metadata changes",
|
|
386
|
+
fields: {
|
|
387
|
+
/** Primary Key (UUID) */
|
|
388
|
+
id: import_data2.Field.text({
|
|
389
|
+
label: "ID",
|
|
390
|
+
required: true,
|
|
391
|
+
readonly: true
|
|
392
|
+
}),
|
|
393
|
+
/** Foreign key to sys_metadata.id */
|
|
394
|
+
metadata_id: import_data2.Field.text({
|
|
395
|
+
label: "Metadata ID",
|
|
396
|
+
required: true,
|
|
397
|
+
readonly: true,
|
|
398
|
+
maxLength: 255
|
|
399
|
+
}),
|
|
400
|
+
/** Machine name (denormalized for easier querying) */
|
|
401
|
+
name: import_data2.Field.text({
|
|
402
|
+
label: "Name",
|
|
403
|
+
required: true,
|
|
404
|
+
searchable: true,
|
|
405
|
+
readonly: true,
|
|
406
|
+
maxLength: 255
|
|
407
|
+
}),
|
|
408
|
+
/** Metadata type (denormalized for easier querying) */
|
|
409
|
+
type: import_data2.Field.text({
|
|
410
|
+
label: "Metadata Type",
|
|
411
|
+
required: true,
|
|
412
|
+
searchable: true,
|
|
413
|
+
readonly: true,
|
|
414
|
+
maxLength: 100
|
|
415
|
+
}),
|
|
416
|
+
/** Version number at this snapshot */
|
|
417
|
+
version: import_data2.Field.number({
|
|
418
|
+
label: "Version",
|
|
419
|
+
required: true,
|
|
420
|
+
readonly: true
|
|
421
|
+
}),
|
|
422
|
+
/** Type of operation that created this history entry */
|
|
423
|
+
operation_type: import_data2.Field.select(["create", "update", "publish", "revert", "delete"], {
|
|
424
|
+
label: "Operation Type",
|
|
425
|
+
required: true,
|
|
426
|
+
readonly: true
|
|
427
|
+
}),
|
|
428
|
+
/** Historical metadata snapshot (JSON payload) */
|
|
429
|
+
metadata: import_data2.Field.textarea({
|
|
430
|
+
label: "Metadata",
|
|
431
|
+
required: true,
|
|
432
|
+
readonly: true,
|
|
433
|
+
description: "JSON-serialized metadata snapshot at this version"
|
|
434
|
+
}),
|
|
435
|
+
/** SHA-256 checksum of metadata content */
|
|
436
|
+
checksum: import_data2.Field.text({
|
|
437
|
+
label: "Checksum",
|
|
438
|
+
required: true,
|
|
439
|
+
readonly: true,
|
|
440
|
+
maxLength: 64
|
|
441
|
+
}),
|
|
442
|
+
/** Checksum of the previous version */
|
|
443
|
+
previous_checksum: import_data2.Field.text({
|
|
444
|
+
label: "Previous Checksum",
|
|
445
|
+
required: false,
|
|
446
|
+
readonly: true,
|
|
447
|
+
maxLength: 64
|
|
448
|
+
}),
|
|
449
|
+
/** Human-readable description of changes */
|
|
450
|
+
change_note: import_data2.Field.textarea({
|
|
451
|
+
label: "Change Note",
|
|
452
|
+
required: false,
|
|
453
|
+
readonly: true,
|
|
454
|
+
description: "Description of what changed in this version"
|
|
455
|
+
}),
|
|
456
|
+
/** Tenant ID for multi-tenant isolation */
|
|
457
|
+
tenant_id: import_data2.Field.text({
|
|
458
|
+
label: "Tenant ID",
|
|
459
|
+
required: false,
|
|
460
|
+
readonly: true,
|
|
461
|
+
maxLength: 255
|
|
462
|
+
}),
|
|
463
|
+
/** User who made this change */
|
|
464
|
+
recorded_by: import_data2.Field.text({
|
|
465
|
+
label: "Recorded By",
|
|
466
|
+
required: false,
|
|
467
|
+
readonly: true,
|
|
468
|
+
maxLength: 255
|
|
469
|
+
}),
|
|
470
|
+
/** When was this version recorded */
|
|
471
|
+
recorded_at: import_data2.Field.datetime({
|
|
472
|
+
label: "Recorded At",
|
|
473
|
+
required: true,
|
|
474
|
+
readonly: true
|
|
475
|
+
})
|
|
476
|
+
},
|
|
477
|
+
indexes: [
|
|
478
|
+
{ fields: ["metadata_id", "version"], unique: true },
|
|
479
|
+
{ fields: ["metadata_id", "recorded_at"] },
|
|
480
|
+
{ fields: ["type", "name"] },
|
|
481
|
+
{ fields: ["recorded_at"] },
|
|
482
|
+
{ fields: ["operation_type"] },
|
|
483
|
+
{ fields: ["tenant_id"] }
|
|
484
|
+
],
|
|
485
|
+
enable: {
|
|
486
|
+
trackHistory: false,
|
|
487
|
+
// Don't track history of history records
|
|
488
|
+
searchable: false,
|
|
489
|
+
apiEnabled: true,
|
|
490
|
+
apiMethods: ["get", "list"],
|
|
491
|
+
// Read-only via API
|
|
492
|
+
trash: false
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// src/utils/metadata-history-utils.ts
|
|
497
|
+
async function calculateChecksum(metadata) {
|
|
498
|
+
const normalized = normalizeJSON(metadata);
|
|
499
|
+
const jsonString = JSON.stringify(normalized);
|
|
500
|
+
if (typeof globalThis.crypto !== "undefined" && globalThis.crypto.subtle) {
|
|
501
|
+
const encoder = new TextEncoder();
|
|
502
|
+
const data = encoder.encode(jsonString);
|
|
503
|
+
const hashBuffer = await globalThis.crypto.subtle.digest("SHA-256", data);
|
|
504
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
505
|
+
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
506
|
+
}
|
|
507
|
+
return simpleHash(jsonString);
|
|
508
|
+
}
|
|
509
|
+
function normalizeJSON(value) {
|
|
510
|
+
if (value === null || value === void 0) {
|
|
511
|
+
return value;
|
|
512
|
+
}
|
|
513
|
+
if (Array.isArray(value)) {
|
|
514
|
+
return value.map(normalizeJSON);
|
|
515
|
+
}
|
|
516
|
+
if (typeof value === "object") {
|
|
517
|
+
const sorted = {};
|
|
518
|
+
const keys = Object.keys(value).sort();
|
|
519
|
+
for (const key of keys) {
|
|
520
|
+
sorted[key] = normalizeJSON(value[key]);
|
|
521
|
+
}
|
|
522
|
+
return sorted;
|
|
523
|
+
}
|
|
524
|
+
return value;
|
|
525
|
+
}
|
|
526
|
+
function simpleHash(str) {
|
|
527
|
+
let hash = 5381;
|
|
528
|
+
for (let i = 0; i < str.length; i++) {
|
|
529
|
+
hash = (hash << 5) + hash + str.charCodeAt(i);
|
|
530
|
+
hash = hash & hash;
|
|
531
|
+
}
|
|
532
|
+
const hexHash = Math.abs(hash).toString(16);
|
|
533
|
+
return hexHash.padStart(64, "0");
|
|
534
|
+
}
|
|
535
|
+
function generateSimpleDiff(oldObj, newObj, path3 = "") {
|
|
536
|
+
const changes = [];
|
|
537
|
+
if (typeof oldObj !== "object" || oldObj === null || typeof newObj !== "object" || newObj === null) {
|
|
538
|
+
if (oldObj !== newObj) {
|
|
539
|
+
changes.push({ op: "replace", path: path3 || "/", value: newObj, oldValue: oldObj });
|
|
540
|
+
}
|
|
541
|
+
return changes;
|
|
542
|
+
}
|
|
543
|
+
if (Array.isArray(oldObj) || Array.isArray(newObj)) {
|
|
544
|
+
if (!Array.isArray(oldObj) || !Array.isArray(newObj) || oldObj.length !== newObj.length) {
|
|
545
|
+
changes.push({ op: "replace", path: path3 || "/", value: newObj, oldValue: oldObj });
|
|
546
|
+
} else {
|
|
547
|
+
for (let i = 0; i < oldObj.length; i++) {
|
|
548
|
+
const subPath = `${path3}/${i}`;
|
|
549
|
+
changes.push(...generateSimpleDiff(oldObj[i], newObj[i], subPath));
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return changes;
|
|
553
|
+
}
|
|
554
|
+
const oldKeys = new Set(Object.keys(oldObj));
|
|
555
|
+
const newKeys = new Set(Object.keys(newObj));
|
|
556
|
+
for (const key of newKeys) {
|
|
557
|
+
if (!oldKeys.has(key)) {
|
|
558
|
+
const subPath = path3 ? `${path3}/${key}` : `/${key}`;
|
|
559
|
+
changes.push({ op: "add", path: subPath, value: newObj[key] });
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
for (const key of oldKeys) {
|
|
563
|
+
if (!newKeys.has(key)) {
|
|
564
|
+
const subPath = path3 ? `${path3}/${key}` : `/${key}`;
|
|
565
|
+
changes.push({ op: "remove", path: subPath, oldValue: oldObj[key] });
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
for (const key of oldKeys) {
|
|
569
|
+
if (newKeys.has(key)) {
|
|
570
|
+
const subPath = path3 ? `${path3}/${key}` : `/${key}`;
|
|
571
|
+
changes.push(...generateSimpleDiff(
|
|
572
|
+
oldObj[key],
|
|
573
|
+
newObj[key],
|
|
574
|
+
subPath
|
|
575
|
+
));
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return changes;
|
|
579
|
+
}
|
|
580
|
+
function generateDiffSummary(diff) {
|
|
581
|
+
if (diff.length === 0) {
|
|
582
|
+
return "No changes";
|
|
583
|
+
}
|
|
584
|
+
const summary = [];
|
|
585
|
+
const addCount = diff.filter((d) => d.op === "add").length;
|
|
586
|
+
const removeCount = diff.filter((d) => d.op === "remove").length;
|
|
587
|
+
const replaceCount = diff.filter((d) => d.op === "replace").length;
|
|
588
|
+
if (addCount > 0) summary.push(`${addCount} field${addCount > 1 ? "s" : ""} added`);
|
|
589
|
+
if (removeCount > 0) summary.push(`${removeCount} field${removeCount > 1 ? "s" : ""} removed`);
|
|
590
|
+
if (replaceCount > 0) summary.push(`${replaceCount} field${replaceCount > 1 ? "s" : ""} modified`);
|
|
591
|
+
return summary.join(", ");
|
|
592
|
+
}
|
|
593
|
+
|
|
370
594
|
// src/loaders/database-loader.ts
|
|
371
595
|
var DatabaseLoader = class {
|
|
372
596
|
constructor(options) {
|
|
@@ -381,9 +605,12 @@ var DatabaseLoader = class {
|
|
|
381
605
|
}
|
|
382
606
|
};
|
|
383
607
|
this.schemaReady = false;
|
|
608
|
+
this.historySchemaReady = false;
|
|
384
609
|
this.driver = options.driver;
|
|
385
610
|
this.tableName = options.tableName ?? "sys_metadata";
|
|
611
|
+
this.historyTableName = options.historyTableName ?? "sys_metadata_history";
|
|
386
612
|
this.tenantId = options.tenantId;
|
|
613
|
+
this.trackHistory = options.trackHistory !== false;
|
|
387
614
|
}
|
|
388
615
|
/**
|
|
389
616
|
* Ensure the metadata table exists.
|
|
@@ -402,6 +629,22 @@ var DatabaseLoader = class {
|
|
|
402
629
|
this.schemaReady = true;
|
|
403
630
|
}
|
|
404
631
|
}
|
|
632
|
+
/**
|
|
633
|
+
* Ensure the history table exists.
|
|
634
|
+
* Uses IDataDriver.syncSchema with the SysMetadataHistoryObject definition.
|
|
635
|
+
*/
|
|
636
|
+
async ensureHistorySchema() {
|
|
637
|
+
if (!this.trackHistory || this.historySchemaReady) return;
|
|
638
|
+
try {
|
|
639
|
+
await this.driver.syncSchema(this.historyTableName, {
|
|
640
|
+
...SysMetadataHistoryObject,
|
|
641
|
+
name: this.historyTableName
|
|
642
|
+
});
|
|
643
|
+
this.historySchemaReady = true;
|
|
644
|
+
} catch (error) {
|
|
645
|
+
console.error("Failed to ensure history schema, will retry on next operation:", error);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
405
648
|
/**
|
|
406
649
|
* Build base filter conditions for queries.
|
|
407
650
|
* Always includes tenantId when configured.
|
|
@@ -416,6 +659,64 @@ var DatabaseLoader = class {
|
|
|
416
659
|
}
|
|
417
660
|
return filter;
|
|
418
661
|
}
|
|
662
|
+
/**
|
|
663
|
+
* Create a history record for a metadata change.
|
|
664
|
+
*
|
|
665
|
+
* @param metadataId - The metadata record ID
|
|
666
|
+
* @param type - Metadata type
|
|
667
|
+
* @param name - Metadata name
|
|
668
|
+
* @param version - Version number
|
|
669
|
+
* @param metadata - The metadata payload
|
|
670
|
+
* @param operationType - Type of operation
|
|
671
|
+
* @param previousChecksum - Checksum of previous version (if any)
|
|
672
|
+
* @param changeNote - Optional change description
|
|
673
|
+
* @param recordedBy - Optional user who made the change
|
|
674
|
+
*/
|
|
675
|
+
async createHistoryRecord(metadataId, type, name, version, metadata, operationType, previousChecksum, changeNote, recordedBy) {
|
|
676
|
+
if (!this.trackHistory) return;
|
|
677
|
+
await this.ensureHistorySchema();
|
|
678
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
679
|
+
const checksum = await calculateChecksum(metadata);
|
|
680
|
+
if (previousChecksum && checksum === previousChecksum && operationType === "update") {
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
const historyId = generateId();
|
|
684
|
+
const metadataJson = JSON.stringify(metadata);
|
|
685
|
+
const historyRecord = {
|
|
686
|
+
id: historyId,
|
|
687
|
+
metadataId,
|
|
688
|
+
name,
|
|
689
|
+
type,
|
|
690
|
+
version,
|
|
691
|
+
operationType,
|
|
692
|
+
metadata: metadataJson,
|
|
693
|
+
checksum,
|
|
694
|
+
previousChecksum,
|
|
695
|
+
changeNote,
|
|
696
|
+
recordedBy,
|
|
697
|
+
recordedAt: now,
|
|
698
|
+
...this.tenantId ? { tenantId: this.tenantId } : {}
|
|
699
|
+
};
|
|
700
|
+
try {
|
|
701
|
+
await this.driver.create(this.historyTableName, {
|
|
702
|
+
id: historyRecord.id,
|
|
703
|
+
metadata_id: historyRecord.metadataId,
|
|
704
|
+
name: historyRecord.name,
|
|
705
|
+
type: historyRecord.type,
|
|
706
|
+
version: historyRecord.version,
|
|
707
|
+
operation_type: historyRecord.operationType,
|
|
708
|
+
metadata: historyRecord.metadata,
|
|
709
|
+
checksum: historyRecord.checksum,
|
|
710
|
+
previous_checksum: historyRecord.previousChecksum,
|
|
711
|
+
change_note: historyRecord.changeNote,
|
|
712
|
+
recorded_by: historyRecord.recordedBy,
|
|
713
|
+
recorded_at: historyRecord.recordedAt,
|
|
714
|
+
...this.tenantId ? { tenant_id: this.tenantId } : {}
|
|
715
|
+
});
|
|
716
|
+
} catch (error) {
|
|
717
|
+
console.error(`Failed to create history record for ${type}/${name}:`, error);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
419
720
|
/**
|
|
420
721
|
* Convert a database row to a metadata payload.
|
|
421
722
|
* Parses the JSON `metadata` column back into an object.
|
|
@@ -543,24 +844,124 @@ var DatabaseLoader = class {
|
|
|
543
844
|
return [];
|
|
544
845
|
}
|
|
545
846
|
}
|
|
847
|
+
/**
|
|
848
|
+
* Fetch a single history snapshot by (type, name, version).
|
|
849
|
+
* Returns null when the record does not exist.
|
|
850
|
+
*/
|
|
851
|
+
async getHistoryRecord(type, name, version) {
|
|
852
|
+
if (!this.trackHistory) return null;
|
|
853
|
+
await this.ensureHistorySchema();
|
|
854
|
+
const metadataRow = await this.driver.findOne(this.tableName, {
|
|
855
|
+
object: this.tableName,
|
|
856
|
+
where: this.baseFilter(type, name)
|
|
857
|
+
});
|
|
858
|
+
if (!metadataRow) return null;
|
|
859
|
+
const filter = {
|
|
860
|
+
metadata_id: metadataRow.id,
|
|
861
|
+
version
|
|
862
|
+
};
|
|
863
|
+
if (this.tenantId) {
|
|
864
|
+
filter.tenant_id = this.tenantId;
|
|
865
|
+
}
|
|
866
|
+
const row = await this.driver.findOne(this.historyTableName, {
|
|
867
|
+
object: this.historyTableName,
|
|
868
|
+
where: filter
|
|
869
|
+
});
|
|
870
|
+
if (!row) return null;
|
|
871
|
+
return {
|
|
872
|
+
id: row.id,
|
|
873
|
+
metadataId: row.metadata_id,
|
|
874
|
+
name: row.name,
|
|
875
|
+
type: row.type,
|
|
876
|
+
version: row.version,
|
|
877
|
+
operationType: row.operation_type,
|
|
878
|
+
metadata: typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata,
|
|
879
|
+
checksum: row.checksum,
|
|
880
|
+
previousChecksum: row.previous_checksum,
|
|
881
|
+
changeNote: row.change_note,
|
|
882
|
+
tenantId: row.tenant_id,
|
|
883
|
+
recordedBy: row.recorded_by,
|
|
884
|
+
recordedAt: row.recorded_at
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Perform a rollback: persist `restoredData` as the new current state and record a
|
|
889
|
+
* single 'revert' history entry (instead of the usual 'update' entry that `save()`
|
|
890
|
+
* would produce). This avoids the duplicate-version problem that arises when
|
|
891
|
+
* `register()` → `save()` writes an 'update' entry followed by an additional
|
|
892
|
+
* 'revert' entry for the same version number.
|
|
893
|
+
*/
|
|
894
|
+
async registerRollback(type, name, restoredData, targetVersion, changeNote, recordedBy) {
|
|
895
|
+
await this.ensureSchema();
|
|
896
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
897
|
+
const metadataJson = JSON.stringify(restoredData);
|
|
898
|
+
const newChecksum = await calculateChecksum(restoredData);
|
|
899
|
+
const existing = await this.driver.findOne(this.tableName, {
|
|
900
|
+
object: this.tableName,
|
|
901
|
+
where: this.baseFilter(type, name)
|
|
902
|
+
});
|
|
903
|
+
if (!existing) {
|
|
904
|
+
throw new Error(`Metadata ${type}/${name} not found for rollback`);
|
|
905
|
+
}
|
|
906
|
+
const previousChecksum = existing.checksum;
|
|
907
|
+
const newVersion = (existing.version ?? 0) + 1;
|
|
908
|
+
await this.driver.update(this.tableName, existing.id, {
|
|
909
|
+
metadata: metadataJson,
|
|
910
|
+
version: newVersion,
|
|
911
|
+
checksum: newChecksum,
|
|
912
|
+
updated_at: now,
|
|
913
|
+
state: "active"
|
|
914
|
+
});
|
|
915
|
+
await this.createHistoryRecord(
|
|
916
|
+
existing.id,
|
|
917
|
+
type,
|
|
918
|
+
name,
|
|
919
|
+
newVersion,
|
|
920
|
+
restoredData,
|
|
921
|
+
"revert",
|
|
922
|
+
previousChecksum,
|
|
923
|
+
changeNote ?? `Rolled back to version ${targetVersion}`,
|
|
924
|
+
recordedBy
|
|
925
|
+
);
|
|
926
|
+
}
|
|
546
927
|
async save(type, name, data, _options) {
|
|
547
928
|
const startTime = Date.now();
|
|
548
929
|
await this.ensureSchema();
|
|
549
930
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
550
931
|
const metadataJson = JSON.stringify(data);
|
|
932
|
+
const newChecksum = await calculateChecksum(data);
|
|
551
933
|
try {
|
|
552
934
|
const existing = await this.driver.findOne(this.tableName, {
|
|
553
935
|
object: this.tableName,
|
|
554
936
|
where: this.baseFilter(type, name)
|
|
555
937
|
});
|
|
556
938
|
if (existing) {
|
|
939
|
+
const previousChecksum = existing.checksum;
|
|
940
|
+
if (newChecksum === previousChecksum) {
|
|
941
|
+
return {
|
|
942
|
+
success: true,
|
|
943
|
+
path: `datasource://${this.tableName}/${type}/${name}`,
|
|
944
|
+
size: metadataJson.length,
|
|
945
|
+
saveTime: Date.now() - startTime
|
|
946
|
+
};
|
|
947
|
+
}
|
|
557
948
|
const version = (existing.version ?? 0) + 1;
|
|
558
949
|
await this.driver.update(this.tableName, existing.id, {
|
|
559
950
|
metadata: metadataJson,
|
|
560
951
|
version,
|
|
952
|
+
checksum: newChecksum,
|
|
561
953
|
updated_at: now,
|
|
562
954
|
state: "active"
|
|
563
955
|
});
|
|
956
|
+
await this.createHistoryRecord(
|
|
957
|
+
existing.id,
|
|
958
|
+
type,
|
|
959
|
+
name,
|
|
960
|
+
version,
|
|
961
|
+
data,
|
|
962
|
+
"update",
|
|
963
|
+
previousChecksum
|
|
964
|
+
);
|
|
564
965
|
return {
|
|
565
966
|
success: true,
|
|
566
967
|
path: `datasource://${this.tableName}/${type}/${name}`,
|
|
@@ -576,6 +977,7 @@ var DatabaseLoader = class {
|
|
|
576
977
|
namespace: "default",
|
|
577
978
|
scope: data?.scope ?? "platform",
|
|
578
979
|
metadata: metadataJson,
|
|
980
|
+
checksum: newChecksum,
|
|
579
981
|
strategy: "merge",
|
|
580
982
|
state: "active",
|
|
581
983
|
version: 1,
|
|
@@ -584,6 +986,14 @@ var DatabaseLoader = class {
|
|
|
584
986
|
created_at: now,
|
|
585
987
|
updated_at: now
|
|
586
988
|
});
|
|
989
|
+
await this.createHistoryRecord(
|
|
990
|
+
id,
|
|
991
|
+
type,
|
|
992
|
+
name,
|
|
993
|
+
1,
|
|
994
|
+
data,
|
|
995
|
+
"create"
|
|
996
|
+
);
|
|
587
997
|
return {
|
|
588
998
|
success: true,
|
|
589
999
|
path: `datasource://${this.tableName}/${type}/${name}`,
|
|
@@ -597,6 +1007,20 @@ var DatabaseLoader = class {
|
|
|
597
1007
|
);
|
|
598
1008
|
}
|
|
599
1009
|
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Delete a metadata item from the database
|
|
1012
|
+
*/
|
|
1013
|
+
async delete(type, name) {
|
|
1014
|
+
await this.ensureSchema();
|
|
1015
|
+
const existing = await this.driver.findOne(this.tableName, {
|
|
1016
|
+
object: this.tableName,
|
|
1017
|
+
where: this.baseFilter(type, name)
|
|
1018
|
+
});
|
|
1019
|
+
if (!existing) {
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
await this.driver.delete(this.tableName, existing.id);
|
|
1023
|
+
}
|
|
600
1024
|
};
|
|
601
1025
|
function generateId() {
|
|
602
1026
|
if (typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.randomUUID === "function") {
|
|
@@ -674,12 +1098,18 @@ var MetadataManager = class {
|
|
|
674
1098
|
// ==========================================
|
|
675
1099
|
/**
|
|
676
1100
|
* Register/save a metadata item by type
|
|
1101
|
+
* Stores in-memory registry and persists to writable loaders (if configured)
|
|
677
1102
|
*/
|
|
678
1103
|
async register(type, name, data) {
|
|
679
1104
|
if (!this.registry.has(type)) {
|
|
680
1105
|
this.registry.set(type, /* @__PURE__ */ new Map());
|
|
681
1106
|
}
|
|
682
1107
|
this.registry.get(type).set(name, data);
|
|
1108
|
+
for (const loader of this.loaders.values()) {
|
|
1109
|
+
if (loader.save) {
|
|
1110
|
+
await loader.save(type, name, data);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
683
1113
|
}
|
|
684
1114
|
/**
|
|
685
1115
|
* Get a metadata item by type and name.
|
|
@@ -730,6 +1160,15 @@ var MetadataManager = class {
|
|
|
730
1160
|
this.registry.delete(type);
|
|
731
1161
|
}
|
|
732
1162
|
}
|
|
1163
|
+
for (const loader of this.loaders.values()) {
|
|
1164
|
+
if (typeof loader.delete === "function") {
|
|
1165
|
+
try {
|
|
1166
|
+
await loader.delete(type, name);
|
|
1167
|
+
} catch (error) {
|
|
1168
|
+
this.logger.warn(`Failed to delete ${type}/${name} from loader ${loader.contract.name}`, { error });
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
733
1172
|
}
|
|
734
1173
|
/**
|
|
735
1174
|
* Check if a metadata item exists
|
|
@@ -812,20 +1251,17 @@ var MetadataManager = class {
|
|
|
812
1251
|
* Unregister all metadata items from a specific package
|
|
813
1252
|
*/
|
|
814
1253
|
async unregisterPackage(packageName) {
|
|
1254
|
+
const itemsToDelete = [];
|
|
815
1255
|
for (const [type, typeStore] of this.registry) {
|
|
816
|
-
const toDelete = [];
|
|
817
1256
|
for (const [name, data] of typeStore) {
|
|
818
1257
|
const meta = data;
|
|
819
1258
|
if (meta?.packageId === packageName || meta?.package === packageName) {
|
|
820
|
-
|
|
1259
|
+
itemsToDelete.push({ type, name });
|
|
821
1260
|
}
|
|
822
1261
|
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
if (typeStore.size === 0) {
|
|
827
|
-
this.registry.delete(type);
|
|
828
|
-
}
|
|
1262
|
+
}
|
|
1263
|
+
for (const { type, name } of itemsToDelete) {
|
|
1264
|
+
await this.unregister(type, name);
|
|
829
1265
|
}
|
|
830
1266
|
}
|
|
831
1267
|
/**
|
|
@@ -1490,6 +1926,174 @@ var MetadataManager = class {
|
|
|
1490
1926
|
}
|
|
1491
1927
|
}
|
|
1492
1928
|
}
|
|
1929
|
+
// ==========================================
|
|
1930
|
+
// Version History & Rollback
|
|
1931
|
+
// ==========================================
|
|
1932
|
+
/**
|
|
1933
|
+
* Get the database loader for history operations.
|
|
1934
|
+
* Returns undefined if no database loader is configured.
|
|
1935
|
+
*/
|
|
1936
|
+
getDatabaseLoader() {
|
|
1937
|
+
const dbLoader = this.loaders.get("database");
|
|
1938
|
+
if (dbLoader && dbLoader instanceof DatabaseLoader) {
|
|
1939
|
+
return dbLoader;
|
|
1940
|
+
}
|
|
1941
|
+
return void 0;
|
|
1942
|
+
}
|
|
1943
|
+
/**
|
|
1944
|
+
* Get version history for a metadata item.
|
|
1945
|
+
* Returns a timeline of all changes made to the item.
|
|
1946
|
+
*/
|
|
1947
|
+
async getHistory(type, name, options) {
|
|
1948
|
+
const dbLoader = this.getDatabaseLoader();
|
|
1949
|
+
if (!dbLoader) {
|
|
1950
|
+
throw new Error("History tracking requires a database loader to be configured");
|
|
1951
|
+
}
|
|
1952
|
+
const driver = dbLoader.driver;
|
|
1953
|
+
const tableName = dbLoader.tableName;
|
|
1954
|
+
const historyTableName = dbLoader.historyTableName;
|
|
1955
|
+
const tenantId = dbLoader.tenantId;
|
|
1956
|
+
const filter = { type, name };
|
|
1957
|
+
if (tenantId) {
|
|
1958
|
+
filter.tenant_id = tenantId;
|
|
1959
|
+
}
|
|
1960
|
+
const metadataRecord = await driver.findOne(tableName, {
|
|
1961
|
+
object: tableName,
|
|
1962
|
+
where: filter
|
|
1963
|
+
});
|
|
1964
|
+
if (!metadataRecord) {
|
|
1965
|
+
return {
|
|
1966
|
+
records: [],
|
|
1967
|
+
total: 0,
|
|
1968
|
+
hasMore: false
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
const historyFilter = {
|
|
1972
|
+
metadata_id: metadataRecord.id
|
|
1973
|
+
};
|
|
1974
|
+
if (tenantId) {
|
|
1975
|
+
historyFilter.tenant_id = tenantId;
|
|
1976
|
+
}
|
|
1977
|
+
if (options?.operationType) {
|
|
1978
|
+
historyFilter.operation_type = options.operationType;
|
|
1979
|
+
}
|
|
1980
|
+
if (options?.since) {
|
|
1981
|
+
historyFilter.recorded_at = { $gte: options.since };
|
|
1982
|
+
}
|
|
1983
|
+
if (options?.until) {
|
|
1984
|
+
if (historyFilter.recorded_at) {
|
|
1985
|
+
historyFilter.recorded_at.$lte = options.until;
|
|
1986
|
+
} else {
|
|
1987
|
+
historyFilter.recorded_at = { $lte: options.until };
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
const limit = options?.limit ?? 50;
|
|
1991
|
+
const offset = options?.offset ?? 0;
|
|
1992
|
+
const historyRecords = await driver.find(historyTableName, {
|
|
1993
|
+
object: historyTableName,
|
|
1994
|
+
where: historyFilter,
|
|
1995
|
+
orderBy: [{ field: "recorded_at", order: "desc" }],
|
|
1996
|
+
limit: limit + 1,
|
|
1997
|
+
// Fetch one extra to determine hasMore
|
|
1998
|
+
offset
|
|
1999
|
+
});
|
|
2000
|
+
const hasMore = historyRecords.length > limit;
|
|
2001
|
+
const records = historyRecords.slice(0, limit);
|
|
2002
|
+
const total = await driver.count(historyTableName, {
|
|
2003
|
+
object: historyTableName,
|
|
2004
|
+
where: historyFilter
|
|
2005
|
+
});
|
|
2006
|
+
const includeMetadata = options?.includeMetadata !== false;
|
|
2007
|
+
const historyResult = records.map((row) => {
|
|
2008
|
+
const parsedMetadata = typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata;
|
|
2009
|
+
return {
|
|
2010
|
+
id: row.id,
|
|
2011
|
+
metadataId: row.metadata_id,
|
|
2012
|
+
name: row.name,
|
|
2013
|
+
type: row.type,
|
|
2014
|
+
version: row.version,
|
|
2015
|
+
operationType: row.operation_type,
|
|
2016
|
+
metadata: includeMetadata ? parsedMetadata : null,
|
|
2017
|
+
checksum: row.checksum,
|
|
2018
|
+
previousChecksum: row.previous_checksum,
|
|
2019
|
+
changeNote: row.change_note,
|
|
2020
|
+
tenantId: row.tenant_id,
|
|
2021
|
+
recordedBy: row.recorded_by,
|
|
2022
|
+
recordedAt: row.recorded_at
|
|
2023
|
+
};
|
|
2024
|
+
});
|
|
2025
|
+
return {
|
|
2026
|
+
records: historyResult,
|
|
2027
|
+
total,
|
|
2028
|
+
hasMore
|
|
2029
|
+
};
|
|
2030
|
+
}
|
|
2031
|
+
/**
|
|
2032
|
+
* Rollback a metadata item to a specific version.
|
|
2033
|
+
* Restores the metadata definition from the history snapshot.
|
|
2034
|
+
*/
|
|
2035
|
+
async rollback(type, name, version, options) {
|
|
2036
|
+
const dbLoader = this.getDatabaseLoader();
|
|
2037
|
+
if (!dbLoader) {
|
|
2038
|
+
throw new Error("Rollback requires a database loader to be configured");
|
|
2039
|
+
}
|
|
2040
|
+
const targetVersion = await dbLoader.getHistoryRecord(type, name, version);
|
|
2041
|
+
if (!targetVersion) {
|
|
2042
|
+
throw new Error(`Version ${version} not found in history for ${type}/${name}`);
|
|
2043
|
+
}
|
|
2044
|
+
if (!targetVersion.metadata) {
|
|
2045
|
+
throw new Error(`Version ${version} metadata snapshot not available`);
|
|
2046
|
+
}
|
|
2047
|
+
const restoredMetadata = targetVersion.metadata;
|
|
2048
|
+
await dbLoader.registerRollback(
|
|
2049
|
+
type,
|
|
2050
|
+
name,
|
|
2051
|
+
restoredMetadata,
|
|
2052
|
+
version,
|
|
2053
|
+
options?.changeNote,
|
|
2054
|
+
options?.recordedBy
|
|
2055
|
+
);
|
|
2056
|
+
if (!this.registry.has(type)) {
|
|
2057
|
+
this.registry.set(type, /* @__PURE__ */ new Map());
|
|
2058
|
+
}
|
|
2059
|
+
this.registry.get(type).set(name, restoredMetadata);
|
|
2060
|
+
return restoredMetadata;
|
|
2061
|
+
}
|
|
2062
|
+
/**
|
|
2063
|
+
* Compare two versions of a metadata item.
|
|
2064
|
+
* Returns a diff showing what changed between versions.
|
|
2065
|
+
*/
|
|
2066
|
+
async diff(type, name, version1, version2) {
|
|
2067
|
+
const dbLoader = this.getDatabaseLoader();
|
|
2068
|
+
if (!dbLoader) {
|
|
2069
|
+
throw new Error("Diff requires a database loader to be configured");
|
|
2070
|
+
}
|
|
2071
|
+
const v1 = await dbLoader.getHistoryRecord(type, name, version1);
|
|
2072
|
+
const v2 = await dbLoader.getHistoryRecord(type, name, version2);
|
|
2073
|
+
if (!v1) {
|
|
2074
|
+
throw new Error(`Version ${version1} not found in history for ${type}/${name}`);
|
|
2075
|
+
}
|
|
2076
|
+
if (!v2) {
|
|
2077
|
+
throw new Error(`Version ${version2} not found in history for ${type}/${name}`);
|
|
2078
|
+
}
|
|
2079
|
+
if (!v1.metadata || !v2.metadata) {
|
|
2080
|
+
throw new Error("Version metadata snapshots not available");
|
|
2081
|
+
}
|
|
2082
|
+
const patch = generateSimpleDiff(v1.metadata, v2.metadata);
|
|
2083
|
+
const identical = patch.length === 0;
|
|
2084
|
+
const summary = generateDiffSummary(patch);
|
|
2085
|
+
return {
|
|
2086
|
+
type,
|
|
2087
|
+
name,
|
|
2088
|
+
version1,
|
|
2089
|
+
version2,
|
|
2090
|
+
checksum1: v1.checksum,
|
|
2091
|
+
checksum2: v2.checksum,
|
|
2092
|
+
identical,
|
|
2093
|
+
patch,
|
|
2094
|
+
summary
|
|
2095
|
+
};
|
|
2096
|
+
}
|
|
1493
2097
|
};
|
|
1494
2098
|
|
|
1495
2099
|
// src/node-metadata-manager.ts
|
|
@@ -1910,14 +2514,17 @@ var MetadataPlugin = class {
|
|
|
1910
2514
|
watch: this.options.watch
|
|
1911
2515
|
});
|
|
1912
2516
|
ctx.registerService("metadata", this.manager);
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
2517
|
+
try {
|
|
2518
|
+
ctx.getService("manifest").register({
|
|
2519
|
+
id: "com.objectstack.metadata",
|
|
2520
|
+
name: "Metadata",
|
|
2521
|
+
version: "1.0.0",
|
|
2522
|
+
type: "plugin",
|
|
2523
|
+
namespace: "sys",
|
|
2524
|
+
objects: [SysMetadataObject]
|
|
2525
|
+
});
|
|
2526
|
+
} catch {
|
|
2527
|
+
}
|
|
1921
2528
|
ctx.logger.info("MetadataPlugin providing metadata service (primary mode)", {
|
|
1922
2529
|
mode: "file-system",
|
|
1923
2530
|
features: ["watch", "persistence", "multi-format", "query", "overlay", "type-registry"]
|
|
@@ -2029,6 +2636,18 @@ var MemoryLoader = class {
|
|
|
2029
2636
|
saveTime: 0
|
|
2030
2637
|
};
|
|
2031
2638
|
}
|
|
2639
|
+
/**
|
|
2640
|
+
* Delete a metadata item from memory storage
|
|
2641
|
+
*/
|
|
2642
|
+
async delete(type, name) {
|
|
2643
|
+
const typeStore = this.storage.get(type);
|
|
2644
|
+
if (typeStore) {
|
|
2645
|
+
typeStore.delete(name);
|
|
2646
|
+
if (typeStore.size === 0) {
|
|
2647
|
+
this.storage.delete(type);
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2032
2651
|
};
|
|
2033
2652
|
|
|
2034
2653
|
// src/loaders/remote-loader.ts
|
|
@@ -2128,6 +2747,330 @@ var RemoteLoader = class {
|
|
|
2128
2747
|
}
|
|
2129
2748
|
};
|
|
2130
2749
|
|
|
2750
|
+
// src/routes/history-routes.ts
|
|
2751
|
+
function registerMetadataHistoryRoutes(app, metadataService) {
|
|
2752
|
+
app.get("/api/v1/metadata/:type/:name/history", async (c) => {
|
|
2753
|
+
if (!metadataService.getHistory) {
|
|
2754
|
+
return c.json({ error: "History tracking not enabled" }, 501);
|
|
2755
|
+
}
|
|
2756
|
+
const { type, name } = c.req.param();
|
|
2757
|
+
const query = c.req.query();
|
|
2758
|
+
try {
|
|
2759
|
+
const options = {};
|
|
2760
|
+
if (query.limit !== void 0) {
|
|
2761
|
+
const limit = parseInt(query.limit, 10);
|
|
2762
|
+
if (!Number.isFinite(limit) || limit < 1) {
|
|
2763
|
+
return c.json({ success: false, error: "limit must be a positive integer" }, 400);
|
|
2764
|
+
}
|
|
2765
|
+
options.limit = limit;
|
|
2766
|
+
}
|
|
2767
|
+
if (query.offset !== void 0) {
|
|
2768
|
+
const offset = parseInt(query.offset, 10);
|
|
2769
|
+
if (!Number.isFinite(offset) || offset < 0) {
|
|
2770
|
+
return c.json({ success: false, error: "offset must be a non-negative integer" }, 400);
|
|
2771
|
+
}
|
|
2772
|
+
options.offset = offset;
|
|
2773
|
+
}
|
|
2774
|
+
if (query.since) options.since = query.since;
|
|
2775
|
+
if (query.until) options.until = query.until;
|
|
2776
|
+
if (query.operationType) options.operationType = query.operationType;
|
|
2777
|
+
if (query.includeMetadata !== void 0) {
|
|
2778
|
+
options.includeMetadata = query.includeMetadata === "true";
|
|
2779
|
+
}
|
|
2780
|
+
const result = await metadataService.getHistory(type, name, options);
|
|
2781
|
+
return c.json({
|
|
2782
|
+
success: true,
|
|
2783
|
+
data: result
|
|
2784
|
+
});
|
|
2785
|
+
} catch (error) {
|
|
2786
|
+
return c.json(
|
|
2787
|
+
{
|
|
2788
|
+
success: false,
|
|
2789
|
+
error: error instanceof Error ? error.message : "Failed to retrieve history"
|
|
2790
|
+
},
|
|
2791
|
+
500
|
|
2792
|
+
);
|
|
2793
|
+
}
|
|
2794
|
+
});
|
|
2795
|
+
app.post("/api/v1/metadata/:type/:name/rollback", async (c) => {
|
|
2796
|
+
if (!metadataService.rollback) {
|
|
2797
|
+
return c.json({ error: "Rollback not supported" }, 501);
|
|
2798
|
+
}
|
|
2799
|
+
const { type, name } = c.req.param();
|
|
2800
|
+
try {
|
|
2801
|
+
const body = await c.req.json();
|
|
2802
|
+
const { version, changeNote, recordedBy } = body;
|
|
2803
|
+
if (typeof version !== "number") {
|
|
2804
|
+
return c.json(
|
|
2805
|
+
{
|
|
2806
|
+
success: false,
|
|
2807
|
+
error: "Version number is required"
|
|
2808
|
+
},
|
|
2809
|
+
400
|
|
2810
|
+
);
|
|
2811
|
+
}
|
|
2812
|
+
const restoredMetadata = await metadataService.rollback(type, name, version, {
|
|
2813
|
+
changeNote,
|
|
2814
|
+
recordedBy
|
|
2815
|
+
});
|
|
2816
|
+
return c.json({
|
|
2817
|
+
success: true,
|
|
2818
|
+
data: {
|
|
2819
|
+
type,
|
|
2820
|
+
name,
|
|
2821
|
+
version,
|
|
2822
|
+
metadata: restoredMetadata
|
|
2823
|
+
}
|
|
2824
|
+
});
|
|
2825
|
+
} catch (error) {
|
|
2826
|
+
return c.json(
|
|
2827
|
+
{
|
|
2828
|
+
success: false,
|
|
2829
|
+
error: error instanceof Error ? error.message : "Rollback failed"
|
|
2830
|
+
},
|
|
2831
|
+
500
|
|
2832
|
+
);
|
|
2833
|
+
}
|
|
2834
|
+
});
|
|
2835
|
+
app.get("/api/v1/metadata/:type/:name/diff", async (c) => {
|
|
2836
|
+
if (!metadataService.diff) {
|
|
2837
|
+
return c.json({ error: "Diff not supported" }, 501);
|
|
2838
|
+
}
|
|
2839
|
+
const { type, name } = c.req.param();
|
|
2840
|
+
const query = c.req.query();
|
|
2841
|
+
try {
|
|
2842
|
+
const version1 = parseInt(query.version1, 10);
|
|
2843
|
+
const version2 = parseInt(query.version2, 10);
|
|
2844
|
+
if (isNaN(version1) || isNaN(version2)) {
|
|
2845
|
+
return c.json(
|
|
2846
|
+
{
|
|
2847
|
+
success: false,
|
|
2848
|
+
error: "Both version1 and version2 query parameters are required"
|
|
2849
|
+
},
|
|
2850
|
+
400
|
|
2851
|
+
);
|
|
2852
|
+
}
|
|
2853
|
+
const diffResult = await metadataService.diff(type, name, version1, version2);
|
|
2854
|
+
return c.json({
|
|
2855
|
+
success: true,
|
|
2856
|
+
data: diffResult
|
|
2857
|
+
});
|
|
2858
|
+
} catch (error) {
|
|
2859
|
+
return c.json(
|
|
2860
|
+
{
|
|
2861
|
+
success: false,
|
|
2862
|
+
error: error instanceof Error ? error.message : "Diff failed"
|
|
2863
|
+
},
|
|
2864
|
+
500
|
|
2865
|
+
);
|
|
2866
|
+
}
|
|
2867
|
+
});
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
// src/utils/history-cleanup.ts
|
|
2871
|
+
var HistoryCleanupManager = class {
|
|
2872
|
+
constructor(policy, dbLoader) {
|
|
2873
|
+
this.policy = policy;
|
|
2874
|
+
this.dbLoader = dbLoader;
|
|
2875
|
+
}
|
|
2876
|
+
/**
|
|
2877
|
+
* Start automatic cleanup if enabled in the policy.
|
|
2878
|
+
*/
|
|
2879
|
+
start() {
|
|
2880
|
+
if (!this.policy.autoCleanup) {
|
|
2881
|
+
return;
|
|
2882
|
+
}
|
|
2883
|
+
const intervalMs = (this.policy.cleanupIntervalHours ?? 24) * 60 * 60 * 1e3;
|
|
2884
|
+
void this.runCleanup();
|
|
2885
|
+
this.cleanupTimer = setInterval(() => {
|
|
2886
|
+
void this.runCleanup();
|
|
2887
|
+
}, intervalMs);
|
|
2888
|
+
}
|
|
2889
|
+
/**
|
|
2890
|
+
* Stop automatic cleanup.
|
|
2891
|
+
*/
|
|
2892
|
+
stop() {
|
|
2893
|
+
if (this.cleanupTimer) {
|
|
2894
|
+
clearInterval(this.cleanupTimer);
|
|
2895
|
+
this.cleanupTimer = void 0;
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
/**
|
|
2899
|
+
* Run cleanup based on the retention policy.
|
|
2900
|
+
* Removes history records that exceed the configured limits.
|
|
2901
|
+
*/
|
|
2902
|
+
async runCleanup() {
|
|
2903
|
+
const driver = this.dbLoader.driver;
|
|
2904
|
+
const historyTableName = this.dbLoader.historyTableName;
|
|
2905
|
+
const tenantId = this.dbLoader.tenantId;
|
|
2906
|
+
let deleted = 0;
|
|
2907
|
+
let errors = 0;
|
|
2908
|
+
try {
|
|
2909
|
+
if (this.policy.maxAgeDays) {
|
|
2910
|
+
const cutoffDate = /* @__PURE__ */ new Date();
|
|
2911
|
+
cutoffDate.setDate(cutoffDate.getDate() - this.policy.maxAgeDays);
|
|
2912
|
+
const cutoffISO = cutoffDate.toISOString();
|
|
2913
|
+
const filter = {
|
|
2914
|
+
recorded_at: { $lt: cutoffISO }
|
|
2915
|
+
};
|
|
2916
|
+
if (tenantId) {
|
|
2917
|
+
filter.tenant_id = tenantId;
|
|
2918
|
+
}
|
|
2919
|
+
try {
|
|
2920
|
+
const result = await this.bulkDeleteByFilter(driver, historyTableName, filter);
|
|
2921
|
+
deleted += result.deleted;
|
|
2922
|
+
errors += result.errors;
|
|
2923
|
+
} catch {
|
|
2924
|
+
errors++;
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
if (this.policy.maxVersions) {
|
|
2928
|
+
try {
|
|
2929
|
+
const metadataIds = await driver.find(historyTableName, {
|
|
2930
|
+
object: historyTableName,
|
|
2931
|
+
where: tenantId ? { tenant_id: tenantId } : {},
|
|
2932
|
+
fields: ["metadata_id"]
|
|
2933
|
+
});
|
|
2934
|
+
const uniqueIds = /* @__PURE__ */ new Set();
|
|
2935
|
+
for (const record of metadataIds) {
|
|
2936
|
+
if (record.metadata_id) {
|
|
2937
|
+
uniqueIds.add(record.metadata_id);
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
for (const metadataId of uniqueIds) {
|
|
2941
|
+
const filter = { metadata_id: metadataId };
|
|
2942
|
+
if (tenantId) {
|
|
2943
|
+
filter.tenant_id = tenantId;
|
|
2944
|
+
}
|
|
2945
|
+
try {
|
|
2946
|
+
const historyRecords = await driver.find(historyTableName, {
|
|
2947
|
+
object: historyTableName,
|
|
2948
|
+
where: filter,
|
|
2949
|
+
orderBy: [{ field: "version", order: "desc" }],
|
|
2950
|
+
fields: ["id"]
|
|
2951
|
+
});
|
|
2952
|
+
if (historyRecords.length > this.policy.maxVersions) {
|
|
2953
|
+
const toDelete = historyRecords.slice(this.policy.maxVersions);
|
|
2954
|
+
const ids = toDelete.map((r) => r.id).filter(Boolean);
|
|
2955
|
+
const result = await this.bulkDeleteByIds(driver, historyTableName, ids);
|
|
2956
|
+
deleted += result.deleted;
|
|
2957
|
+
errors += result.errors;
|
|
2958
|
+
}
|
|
2959
|
+
} catch {
|
|
2960
|
+
errors++;
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
} catch {
|
|
2964
|
+
errors++;
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
} catch (error) {
|
|
2968
|
+
console.error("History cleanup failed:", error);
|
|
2969
|
+
errors++;
|
|
2970
|
+
}
|
|
2971
|
+
return { deleted, errors };
|
|
2972
|
+
}
|
|
2973
|
+
/**
|
|
2974
|
+
* Delete records matching a filter using the most efficient method available on the driver.
|
|
2975
|
+
*/
|
|
2976
|
+
async bulkDeleteByFilter(driver, table, filter) {
|
|
2977
|
+
const driverAny = driver;
|
|
2978
|
+
if (typeof driverAny.deleteMany === "function") {
|
|
2979
|
+
const count = await driverAny.deleteMany(table, filter);
|
|
2980
|
+
return { deleted: typeof count === "number" ? count : 0, errors: 0 };
|
|
2981
|
+
}
|
|
2982
|
+
const records = await driver.find(table, { object: table, where: filter, fields: ["id"] });
|
|
2983
|
+
const ids = records.map((r) => r.id).filter(Boolean);
|
|
2984
|
+
return this.bulkDeleteByIds(driver, table, ids);
|
|
2985
|
+
}
|
|
2986
|
+
/**
|
|
2987
|
+
* Delete records by IDs using bulkDelete when available, otherwise one-by-one.
|
|
2988
|
+
*/
|
|
2989
|
+
async bulkDeleteByIds(driver, table, ids) {
|
|
2990
|
+
if (ids.length === 0) return { deleted: 0, errors: 0 };
|
|
2991
|
+
const driverAny = driver;
|
|
2992
|
+
if (typeof driverAny.bulkDelete === "function") {
|
|
2993
|
+
const result = await driverAny.bulkDelete(table, ids);
|
|
2994
|
+
return {
|
|
2995
|
+
deleted: typeof result === "number" ? result : ids.length,
|
|
2996
|
+
errors: 0
|
|
2997
|
+
};
|
|
2998
|
+
}
|
|
2999
|
+
let deleted = 0;
|
|
3000
|
+
let errors = 0;
|
|
3001
|
+
for (const id of ids) {
|
|
3002
|
+
try {
|
|
3003
|
+
await driver.delete(table, id);
|
|
3004
|
+
deleted++;
|
|
3005
|
+
} catch {
|
|
3006
|
+
errors++;
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
return { deleted, errors };
|
|
3010
|
+
}
|
|
3011
|
+
/**
|
|
3012
|
+
* Get cleanup statistics without actually deleting anything.
|
|
3013
|
+
* Useful for previewing what would be cleaned up.
|
|
3014
|
+
*/
|
|
3015
|
+
async getCleanupStats() {
|
|
3016
|
+
const driver = this.dbLoader.driver;
|
|
3017
|
+
const historyTableName = this.dbLoader.historyTableName;
|
|
3018
|
+
const tenantId = this.dbLoader.tenantId;
|
|
3019
|
+
let recordsByAge = 0;
|
|
3020
|
+
let recordsByCount = 0;
|
|
3021
|
+
try {
|
|
3022
|
+
if (this.policy.maxAgeDays) {
|
|
3023
|
+
const cutoffDate = /* @__PURE__ */ new Date();
|
|
3024
|
+
cutoffDate.setDate(cutoffDate.getDate() - this.policy.maxAgeDays);
|
|
3025
|
+
const cutoffISO = cutoffDate.toISOString();
|
|
3026
|
+
const filter = {
|
|
3027
|
+
recorded_at: { $lt: cutoffISO }
|
|
3028
|
+
};
|
|
3029
|
+
if (tenantId) {
|
|
3030
|
+
filter.tenant_id = tenantId;
|
|
3031
|
+
}
|
|
3032
|
+
recordsByAge = await driver.count(historyTableName, {
|
|
3033
|
+
object: historyTableName,
|
|
3034
|
+
where: filter
|
|
3035
|
+
});
|
|
3036
|
+
}
|
|
3037
|
+
if (this.policy.maxVersions) {
|
|
3038
|
+
const metadataIds = await driver.find(historyTableName, {
|
|
3039
|
+
object: historyTableName,
|
|
3040
|
+
where: tenantId ? { tenant_id: tenantId } : {},
|
|
3041
|
+
fields: ["metadata_id"]
|
|
3042
|
+
});
|
|
3043
|
+
const uniqueIds = /* @__PURE__ */ new Set();
|
|
3044
|
+
for (const record of metadataIds) {
|
|
3045
|
+
if (record.metadata_id) {
|
|
3046
|
+
uniqueIds.add(record.metadata_id);
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
for (const metadataId of uniqueIds) {
|
|
3050
|
+
const filter = { metadata_id: metadataId };
|
|
3051
|
+
if (tenantId) {
|
|
3052
|
+
filter.tenant_id = tenantId;
|
|
3053
|
+
}
|
|
3054
|
+
const count = await driver.count(historyTableName, {
|
|
3055
|
+
object: historyTableName,
|
|
3056
|
+
where: filter
|
|
3057
|
+
});
|
|
3058
|
+
if (count > this.policy.maxVersions) {
|
|
3059
|
+
recordsByCount += count - this.policy.maxVersions;
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
} catch (error) {
|
|
3064
|
+
console.error("Failed to get cleanup stats:", error);
|
|
3065
|
+
}
|
|
3066
|
+
return {
|
|
3067
|
+
recordsByAge,
|
|
3068
|
+
recordsByCount,
|
|
3069
|
+
total: recordsByAge + recordsByCount
|
|
3070
|
+
};
|
|
3071
|
+
}
|
|
3072
|
+
};
|
|
3073
|
+
|
|
2131
3074
|
// src/migration/index.ts
|
|
2132
3075
|
var migration_exports = {};
|
|
2133
3076
|
__export(migration_exports, {
|
|
@@ -2187,6 +3130,7 @@ var MigrationExecutor = class {
|
|
|
2187
3130
|
0 && (module.exports = {
|
|
2188
3131
|
DatabaseLoader,
|
|
2189
3132
|
FilesystemLoader,
|
|
3133
|
+
HistoryCleanupManager,
|
|
2190
3134
|
JSONSerializer,
|
|
2191
3135
|
MemoryLoader,
|
|
2192
3136
|
MetadataManager,
|
|
@@ -2194,8 +3138,13 @@ var MigrationExecutor = class {
|
|
|
2194
3138
|
Migration,
|
|
2195
3139
|
NodeMetadataManager,
|
|
2196
3140
|
RemoteLoader,
|
|
3141
|
+
SysMetadataHistoryObject,
|
|
2197
3142
|
SysMetadataObject,
|
|
2198
3143
|
TypeScriptSerializer,
|
|
2199
|
-
YAMLSerializer
|
|
3144
|
+
YAMLSerializer,
|
|
3145
|
+
calculateChecksum,
|
|
3146
|
+
generateDiffSummary,
|
|
3147
|
+
generateSimpleDiff,
|
|
3148
|
+
registerMetadataHistoryRoutes
|
|
2200
3149
|
});
|
|
2201
3150
|
//# sourceMappingURL=node.cjs.map
|