@objectstack/metadata 4.0.4 → 4.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/README.md +99 -12
- package/dist/index.cjs +793 -534
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +208 -6151
- package/dist/index.d.ts +208 -6151
- package/dist/index.js +794 -532
- package/dist/index.js.map +1 -1
- package/dist/migrations/index.cjs +156 -0
- package/dist/migrations/index.cjs.map +1 -0
- package/dist/migrations/index.d.cts +103 -0
- package/dist/migrations/index.d.ts +103 -0
- package/dist/migrations/index.js +127 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/node.cjs +793 -534
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +2 -3
- package/dist/node.d.ts +2 -3
- package/dist/node.js +794 -532
- package/dist/node.js.map +1 -1
- package/package.json +28 -8
package/dist/node.cjs
CHANGED
|
@@ -40,8 +40,8 @@ __export(node_exports, {
|
|
|
40
40
|
Migration: () => migration_exports,
|
|
41
41
|
NodeMetadataManager: () => NodeMetadataManager,
|
|
42
42
|
RemoteLoader: () => RemoteLoader,
|
|
43
|
-
SysMetadataHistoryObject: () => SysMetadataHistoryObject,
|
|
44
|
-
SysMetadataObject: () => SysMetadataObject,
|
|
43
|
+
SysMetadataHistoryObject: () => import_metadata3.SysMetadataHistoryObject,
|
|
44
|
+
SysMetadataObject: () => import_metadata3.SysMetadataObject,
|
|
45
45
|
TypeScriptSerializer: () => TypeScriptSerializer,
|
|
46
46
|
YAMLSerializer: () => YAMLSerializer,
|
|
47
47
|
calculateChecksum: () => calculateChecksum,
|
|
@@ -221,277 +221,8 @@ export default metadata;
|
|
|
221
221
|
}
|
|
222
222
|
};
|
|
223
223
|
|
|
224
|
-
// src/
|
|
225
|
-
var
|
|
226
|
-
var SysMetadataObject = import_data.ObjectSchema.create({
|
|
227
|
-
namespace: "sys",
|
|
228
|
-
name: "metadata",
|
|
229
|
-
label: "System Metadata",
|
|
230
|
-
pluralLabel: "System Metadata",
|
|
231
|
-
icon: "settings",
|
|
232
|
-
isSystem: true,
|
|
233
|
-
description: "Stores platform and user-scope metadata records (objects, views, flows, etc.)",
|
|
234
|
-
fields: {
|
|
235
|
-
/** Primary Key (UUID) */
|
|
236
|
-
id: import_data.Field.text({
|
|
237
|
-
label: "ID",
|
|
238
|
-
required: true,
|
|
239
|
-
readonly: true
|
|
240
|
-
}),
|
|
241
|
-
/** Machine name — unique identifier used in code references */
|
|
242
|
-
name: import_data.Field.text({
|
|
243
|
-
label: "Name",
|
|
244
|
-
required: true,
|
|
245
|
-
searchable: true,
|
|
246
|
-
maxLength: 255
|
|
247
|
-
}),
|
|
248
|
-
/** Metadata type (e.g. "object", "view", "flow") */
|
|
249
|
-
type: import_data.Field.text({
|
|
250
|
-
label: "Metadata Type",
|
|
251
|
-
required: true,
|
|
252
|
-
searchable: true,
|
|
253
|
-
maxLength: 100
|
|
254
|
-
}),
|
|
255
|
-
/** Namespace / module grouping (e.g. "crm", "core") */
|
|
256
|
-
namespace: import_data.Field.text({
|
|
257
|
-
label: "Namespace",
|
|
258
|
-
required: false,
|
|
259
|
-
defaultValue: "default",
|
|
260
|
-
maxLength: 100
|
|
261
|
-
}),
|
|
262
|
-
/** Package that owns/delivered this metadata */
|
|
263
|
-
package_id: import_data.Field.text({
|
|
264
|
-
label: "Package ID",
|
|
265
|
-
required: false,
|
|
266
|
-
maxLength: 255
|
|
267
|
-
}),
|
|
268
|
-
/** Who manages this record: package, platform, or user */
|
|
269
|
-
managed_by: import_data.Field.select(["package", "platform", "user"], {
|
|
270
|
-
label: "Managed By",
|
|
271
|
-
required: false
|
|
272
|
-
}),
|
|
273
|
-
/** Scope: system (code), platform (admin DB), user (personal DB) */
|
|
274
|
-
scope: import_data.Field.select(["system", "platform", "user"], {
|
|
275
|
-
label: "Scope",
|
|
276
|
-
required: true,
|
|
277
|
-
defaultValue: "platform"
|
|
278
|
-
}),
|
|
279
|
-
/** JSON payload — the actual metadata configuration */
|
|
280
|
-
metadata: import_data.Field.textarea({
|
|
281
|
-
label: "Metadata",
|
|
282
|
-
required: true,
|
|
283
|
-
description: "JSON-serialized metadata payload"
|
|
284
|
-
}),
|
|
285
|
-
/** Parent metadata name for extension/override */
|
|
286
|
-
extends: import_data.Field.text({
|
|
287
|
-
label: "Extends",
|
|
288
|
-
required: false,
|
|
289
|
-
maxLength: 255
|
|
290
|
-
}),
|
|
291
|
-
/** Merge strategy when extending parent metadata */
|
|
292
|
-
strategy: import_data.Field.select(["merge", "replace"], {
|
|
293
|
-
label: "Strategy",
|
|
294
|
-
required: false,
|
|
295
|
-
defaultValue: "merge"
|
|
296
|
-
}),
|
|
297
|
-
/** Owner user ID (for user-scope items) */
|
|
298
|
-
owner: import_data.Field.text({
|
|
299
|
-
label: "Owner",
|
|
300
|
-
required: false,
|
|
301
|
-
maxLength: 255
|
|
302
|
-
}),
|
|
303
|
-
/** Lifecycle state */
|
|
304
|
-
state: import_data.Field.select(["draft", "active", "archived", "deprecated"], {
|
|
305
|
-
label: "State",
|
|
306
|
-
required: false,
|
|
307
|
-
defaultValue: "active"
|
|
308
|
-
}),
|
|
309
|
-
/** Tenant ID for multi-tenant isolation */
|
|
310
|
-
tenant_id: import_data.Field.text({
|
|
311
|
-
label: "Tenant ID",
|
|
312
|
-
required: false,
|
|
313
|
-
maxLength: 255
|
|
314
|
-
}),
|
|
315
|
-
/** Version number for optimistic concurrency */
|
|
316
|
-
version: import_data.Field.number({
|
|
317
|
-
label: "Version",
|
|
318
|
-
required: false,
|
|
319
|
-
defaultValue: 1
|
|
320
|
-
}),
|
|
321
|
-
/** Content checksum for change detection */
|
|
322
|
-
checksum: import_data.Field.text({
|
|
323
|
-
label: "Checksum",
|
|
324
|
-
required: false,
|
|
325
|
-
maxLength: 64
|
|
326
|
-
}),
|
|
327
|
-
/** Origin of this metadata record */
|
|
328
|
-
source: import_data.Field.select(["filesystem", "database", "api", "migration"], {
|
|
329
|
-
label: "Source",
|
|
330
|
-
required: false
|
|
331
|
-
}),
|
|
332
|
-
/** Classification tags (JSON array) */
|
|
333
|
-
tags: import_data.Field.textarea({
|
|
334
|
-
label: "Tags",
|
|
335
|
-
required: false,
|
|
336
|
-
description: "JSON-serialized array of classification tags"
|
|
337
|
-
}),
|
|
338
|
-
/** Audit fields */
|
|
339
|
-
created_by: import_data.Field.text({
|
|
340
|
-
label: "Created By",
|
|
341
|
-
required: false,
|
|
342
|
-
readonly: true,
|
|
343
|
-
maxLength: 255
|
|
344
|
-
}),
|
|
345
|
-
created_at: import_data.Field.datetime({
|
|
346
|
-
label: "Created At",
|
|
347
|
-
required: false,
|
|
348
|
-
readonly: true
|
|
349
|
-
}),
|
|
350
|
-
updated_by: import_data.Field.text({
|
|
351
|
-
label: "Updated By",
|
|
352
|
-
required: false,
|
|
353
|
-
maxLength: 255
|
|
354
|
-
}),
|
|
355
|
-
updated_at: import_data.Field.datetime({
|
|
356
|
-
label: "Updated At",
|
|
357
|
-
required: false
|
|
358
|
-
})
|
|
359
|
-
},
|
|
360
|
-
indexes: [
|
|
361
|
-
{ fields: ["type", "name"], unique: true },
|
|
362
|
-
{ fields: ["type", "scope"] },
|
|
363
|
-
{ fields: ["tenant_id"] },
|
|
364
|
-
{ fields: ["state"] },
|
|
365
|
-
{ fields: ["namespace"] }
|
|
366
|
-
],
|
|
367
|
-
enable: {
|
|
368
|
-
trackHistory: true,
|
|
369
|
-
searchable: false,
|
|
370
|
-
apiEnabled: true,
|
|
371
|
-
apiMethods: ["get", "list", "create", "update", "delete"],
|
|
372
|
-
trash: false
|
|
373
|
-
}
|
|
374
|
-
});
|
|
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
|
-
});
|
|
224
|
+
// src/loaders/database-loader.ts
|
|
225
|
+
var import_metadata = require("@objectstack/platform-objects/metadata");
|
|
495
226
|
|
|
496
227
|
// src/utils/metadata-history-utils.ts
|
|
497
228
|
async function calculateChecksum(metadata) {
|
|
@@ -591,6 +322,114 @@ function generateDiffSummary(diff) {
|
|
|
591
322
|
return summary.join(", ");
|
|
592
323
|
}
|
|
593
324
|
|
|
325
|
+
// src/utils/lru-cache.ts
|
|
326
|
+
var LRUCache = class {
|
|
327
|
+
constructor(options = {}) {
|
|
328
|
+
this.map = /* @__PURE__ */ new Map();
|
|
329
|
+
this.hits = 0;
|
|
330
|
+
this.misses = 0;
|
|
331
|
+
this.maxSize = options.maxSize && options.maxSize > 0 ? options.maxSize : 0;
|
|
332
|
+
this.ttl = options.ttl && options.ttl > 0 ? options.ttl : 0;
|
|
333
|
+
}
|
|
334
|
+
get(key) {
|
|
335
|
+
const entry = this.map.get(key);
|
|
336
|
+
if (!entry) {
|
|
337
|
+
this.misses++;
|
|
338
|
+
return void 0;
|
|
339
|
+
}
|
|
340
|
+
if (entry.expiresAt !== 0 && entry.expiresAt <= Date.now()) {
|
|
341
|
+
this.map.delete(key);
|
|
342
|
+
this.misses++;
|
|
343
|
+
return void 0;
|
|
344
|
+
}
|
|
345
|
+
this.map.delete(key);
|
|
346
|
+
this.map.set(key, entry);
|
|
347
|
+
this.hits++;
|
|
348
|
+
return entry.value;
|
|
349
|
+
}
|
|
350
|
+
set(key, value) {
|
|
351
|
+
if (this.map.has(key)) {
|
|
352
|
+
this.map.delete(key);
|
|
353
|
+
} else if (this.maxSize > 0 && this.map.size >= this.maxSize) {
|
|
354
|
+
const oldest = this.map.keys().next();
|
|
355
|
+
if (!oldest.done) this.map.delete(oldest.value);
|
|
356
|
+
}
|
|
357
|
+
this.map.set(key, {
|
|
358
|
+
value,
|
|
359
|
+
expiresAt: this.ttl > 0 ? Date.now() + this.ttl : 0
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
has(key) {
|
|
363
|
+
return this.get(key) !== void 0;
|
|
364
|
+
}
|
|
365
|
+
delete(key) {
|
|
366
|
+
return this.map.delete(key);
|
|
367
|
+
}
|
|
368
|
+
clear() {
|
|
369
|
+
this.map.clear();
|
|
370
|
+
}
|
|
371
|
+
get size() {
|
|
372
|
+
return this.map.size;
|
|
373
|
+
}
|
|
374
|
+
/** Diagnostic counters — useful for `metrics` endpoints. */
|
|
375
|
+
stats() {
|
|
376
|
+
const total = this.hits + this.misses;
|
|
377
|
+
return {
|
|
378
|
+
size: this.map.size,
|
|
379
|
+
hits: this.hits,
|
|
380
|
+
misses: this.misses,
|
|
381
|
+
hitRate: total === 0 ? 0 : this.hits / total
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
/** Resets hit/miss counters without dropping cached entries. */
|
|
385
|
+
resetStats() {
|
|
386
|
+
this.hits = 0;
|
|
387
|
+
this.misses = 0;
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
// src/migrations/add-sys-metadata-overlay-index.ts
|
|
392
|
+
var INDEX_NAME = "idx_sys_metadata_overlay_active";
|
|
393
|
+
var TABLE = "sys_metadata";
|
|
394
|
+
var COLUMNS = "(type, name, organization_id, project_id, scope)";
|
|
395
|
+
var WHERE = "state = 'active'";
|
|
396
|
+
async function addSysMetadataOverlayIndex(driver) {
|
|
397
|
+
const driverAny = driver;
|
|
398
|
+
const exec = async (sql) => {
|
|
399
|
+
if (typeof driverAny.raw === "function") {
|
|
400
|
+
await driverAny.raw(sql);
|
|
401
|
+
} else if (typeof driverAny.execute === "function") {
|
|
402
|
+
await driverAny.execute(sql);
|
|
403
|
+
} else {
|
|
404
|
+
throw new Error("driver has neither raw nor execute");
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
const partialSql = `CREATE UNIQUE INDEX IF NOT EXISTS ${INDEX_NAME} ON ${TABLE} ${COLUMNS} WHERE ${WHERE}`;
|
|
408
|
+
const fallbackSql = `CREATE INDEX IF NOT EXISTS ${INDEX_NAME} ON ${TABLE} ${COLUMNS}`;
|
|
409
|
+
try {
|
|
410
|
+
await exec(partialSql);
|
|
411
|
+
return { index: INDEX_NAME, status: "created" };
|
|
412
|
+
} catch (err) {
|
|
413
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
414
|
+
if (/partial|where clause|syntax/i.test(msg)) {
|
|
415
|
+
try {
|
|
416
|
+
await exec(fallbackSql);
|
|
417
|
+
return { index: INDEX_NAME, status: "fallback_non_unique" };
|
|
418
|
+
} catch (fallbackErr) {
|
|
419
|
+
return {
|
|
420
|
+
index: INDEX_NAME,
|
|
421
|
+
status: "error",
|
|
422
|
+
error: fallbackErr instanceof Error ? fallbackErr.message : String(fallbackErr)
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (/already exists/i.test(msg)) {
|
|
427
|
+
return { index: INDEX_NAME, status: "already_exists" };
|
|
428
|
+
}
|
|
429
|
+
return { index: INDEX_NAME, status: "error", error: msg };
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
594
433
|
// src/loaders/database-loader.ts
|
|
595
434
|
var DatabaseLoader = class {
|
|
596
435
|
constructor(options) {
|
|
@@ -606,11 +445,103 @@ var DatabaseLoader = class {
|
|
|
606
445
|
};
|
|
607
446
|
this.schemaReady = false;
|
|
608
447
|
this.historySchemaReady = false;
|
|
448
|
+
if (!options.driver && !options.engine) {
|
|
449
|
+
throw new Error("DatabaseLoader requires either a driver or engine");
|
|
450
|
+
}
|
|
609
451
|
this.driver = options.driver;
|
|
452
|
+
this.engine = options.engine;
|
|
610
453
|
this.tableName = options.tableName ?? "sys_metadata";
|
|
611
454
|
this.historyTableName = options.historyTableName ?? "sys_metadata_history";
|
|
612
|
-
this.
|
|
455
|
+
this.organizationId = options.organizationId;
|
|
456
|
+
this.projectId = options.projectId;
|
|
613
457
|
this.trackHistory = options.trackHistory !== false;
|
|
458
|
+
const cacheOpts = options.cache;
|
|
459
|
+
const cacheEnabled = cacheOpts?.enabled !== false;
|
|
460
|
+
if (cacheEnabled) {
|
|
461
|
+
const lruOpts = {
|
|
462
|
+
maxSize: cacheOpts?.maxSize ?? 500,
|
|
463
|
+
ttl: cacheOpts?.ttl ?? 6e4
|
|
464
|
+
};
|
|
465
|
+
this.loadCache = new LRUCache(lruOpts);
|
|
466
|
+
this.loadManyCache = new LRUCache(lruOpts);
|
|
467
|
+
this.listCache = new LRUCache(lruOpts);
|
|
468
|
+
this.statCache = new LRUCache(lruOpts);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
// ==========================================
|
|
472
|
+
// Cache helpers
|
|
473
|
+
// ==========================================
|
|
474
|
+
cacheKey(type, name) {
|
|
475
|
+
return `${type}::${name}`;
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Invalidate all cached entries for a specific (type, name) pair plus
|
|
479
|
+
* the type-level aggregates (`loadMany`, `list`). Called from every write
|
|
480
|
+
* path (`save`, `delete`, `registerRollback`).
|
|
481
|
+
*/
|
|
482
|
+
invalidate(type, name) {
|
|
483
|
+
if (!this.loadCache) return;
|
|
484
|
+
const key = this.cacheKey(type, name);
|
|
485
|
+
this.loadCache.delete(key);
|
|
486
|
+
this.statCache?.delete(key);
|
|
487
|
+
this.loadManyCache?.delete(type);
|
|
488
|
+
this.listCache?.delete(type);
|
|
489
|
+
}
|
|
490
|
+
/** Drop the entire cache — useful after bulk imports or schema changes. */
|
|
491
|
+
invalidateAll() {
|
|
492
|
+
this.loadCache?.clear();
|
|
493
|
+
this.loadManyCache?.clear();
|
|
494
|
+
this.listCache?.clear();
|
|
495
|
+
this.statCache?.clear();
|
|
496
|
+
}
|
|
497
|
+
/** Diagnostic: aggregated cache statistics for `metrics` endpoints. */
|
|
498
|
+
getCacheStats() {
|
|
499
|
+
return {
|
|
500
|
+
enabled: this.loadCache !== void 0,
|
|
501
|
+
load: this.loadCache?.stats() ?? null,
|
|
502
|
+
loadMany: this.loadManyCache?.stats() ?? null,
|
|
503
|
+
list: this.listCache?.stats() ?? null,
|
|
504
|
+
stat: this.statCache?.stats() ?? null
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
// ==========================================
|
|
508
|
+
// Internal CRUD helpers (driver vs engine)
|
|
509
|
+
// ==========================================
|
|
510
|
+
async _find(table, query) {
|
|
511
|
+
if (this.engine) {
|
|
512
|
+
return this.engine.find(table, query);
|
|
513
|
+
}
|
|
514
|
+
return this.driver.find(table, { object: table, ...query });
|
|
515
|
+
}
|
|
516
|
+
async _findOne(table, query) {
|
|
517
|
+
if (this.engine) {
|
|
518
|
+
return this.engine.findOne(table, query);
|
|
519
|
+
}
|
|
520
|
+
return this.driver.findOne(table, { object: table, ...query });
|
|
521
|
+
}
|
|
522
|
+
async _count(table, query) {
|
|
523
|
+
if (this.engine) {
|
|
524
|
+
return this.engine.count(table, query);
|
|
525
|
+
}
|
|
526
|
+
return this.driver.count(table, { object: table, ...query });
|
|
527
|
+
}
|
|
528
|
+
async _create(table, data) {
|
|
529
|
+
if (this.engine) {
|
|
530
|
+
return this.engine.insert(table, data);
|
|
531
|
+
}
|
|
532
|
+
return this.driver.create(table, data);
|
|
533
|
+
}
|
|
534
|
+
async _update(table, id, data) {
|
|
535
|
+
if (this.engine) {
|
|
536
|
+
return this.engine.update(table, { id, ...data });
|
|
537
|
+
}
|
|
538
|
+
return this.driver.update(table, id, data);
|
|
539
|
+
}
|
|
540
|
+
async _delete(table, id) {
|
|
541
|
+
if (this.engine) {
|
|
542
|
+
return this.engine.delete(table, { where: { id } });
|
|
543
|
+
}
|
|
544
|
+
return this.driver.delete(table, id);
|
|
614
545
|
}
|
|
615
546
|
/**
|
|
616
547
|
* Ensure the metadata table exists.
|
|
@@ -619,12 +550,37 @@ var DatabaseLoader = class {
|
|
|
619
550
|
*/
|
|
620
551
|
async ensureSchema() {
|
|
621
552
|
if (this.schemaReady) return;
|
|
553
|
+
if (this.engine) {
|
|
554
|
+
this.schemaReady = true;
|
|
555
|
+
try {
|
|
556
|
+
const engineAny = this.engine;
|
|
557
|
+
let driver = engineAny?.driver ?? engineAny?.getDriver?.();
|
|
558
|
+
if (!driver && engineAny?.drivers instanceof Map) {
|
|
559
|
+
for (const candidate of engineAny.drivers.values()) {
|
|
560
|
+
const c = candidate;
|
|
561
|
+
if (c && (typeof c.raw === "function" || typeof c.execute === "function")) {
|
|
562
|
+
driver = candidate;
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (driver) {
|
|
568
|
+
await addSysMetadataOverlayIndex(driver);
|
|
569
|
+
}
|
|
570
|
+
} catch {
|
|
571
|
+
}
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
622
574
|
try {
|
|
623
575
|
await this.driver.syncSchema(this.tableName, {
|
|
624
|
-
...SysMetadataObject,
|
|
576
|
+
...import_metadata.SysMetadataObject,
|
|
625
577
|
name: this.tableName
|
|
626
578
|
});
|
|
627
579
|
this.schemaReady = true;
|
|
580
|
+
try {
|
|
581
|
+
await addSysMetadataOverlayIndex(this.driver);
|
|
582
|
+
} catch {
|
|
583
|
+
}
|
|
628
584
|
} catch {
|
|
629
585
|
this.schemaReady = true;
|
|
630
586
|
}
|
|
@@ -635,9 +591,13 @@ var DatabaseLoader = class {
|
|
|
635
591
|
*/
|
|
636
592
|
async ensureHistorySchema() {
|
|
637
593
|
if (!this.trackHistory || this.historySchemaReady) return;
|
|
594
|
+
if (this.engine) {
|
|
595
|
+
this.historySchemaReady = true;
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
638
598
|
try {
|
|
639
599
|
await this.driver.syncSchema(this.historyTableName, {
|
|
640
|
-
...SysMetadataHistoryObject,
|
|
600
|
+
...import_metadata.SysMetadataHistoryObject,
|
|
641
601
|
name: this.historyTableName
|
|
642
602
|
});
|
|
643
603
|
this.historySchemaReady = true;
|
|
@@ -647,16 +607,18 @@ var DatabaseLoader = class {
|
|
|
647
607
|
}
|
|
648
608
|
/**
|
|
649
609
|
* Build base filter conditions for queries.
|
|
650
|
-
*
|
|
610
|
+
* Filters by organizationId when configured; project_id when projectId is set,
|
|
611
|
+
* or null (platform-global) when not set.
|
|
651
612
|
*/
|
|
652
613
|
baseFilter(type, name) {
|
|
653
614
|
const filter = { type };
|
|
654
615
|
if (name !== void 0) {
|
|
655
616
|
filter.name = name;
|
|
656
617
|
}
|
|
657
|
-
if (this.
|
|
658
|
-
filter.
|
|
618
|
+
if (this.organizationId) {
|
|
619
|
+
filter.organization_id = this.organizationId;
|
|
659
620
|
}
|
|
621
|
+
filter.project_id = this.projectId ?? null;
|
|
660
622
|
return filter;
|
|
661
623
|
}
|
|
662
624
|
/**
|
|
@@ -695,10 +657,11 @@ var DatabaseLoader = class {
|
|
|
695
657
|
changeNote,
|
|
696
658
|
recordedBy,
|
|
697
659
|
recordedAt: now,
|
|
698
|
-
...this.
|
|
660
|
+
...this.organizationId ? { organizationId: this.organizationId } : {},
|
|
661
|
+
...this.projectId !== void 0 ? { projectId: this.projectId } : {}
|
|
699
662
|
};
|
|
700
663
|
try {
|
|
701
|
-
await this.
|
|
664
|
+
await this._create(this.historyTableName, {
|
|
702
665
|
id: historyRecord.id,
|
|
703
666
|
metadata_id: historyRecord.metadataId,
|
|
704
667
|
name: historyRecord.name,
|
|
@@ -711,7 +674,8 @@ var DatabaseLoader = class {
|
|
|
711
674
|
change_note: historyRecord.changeNote,
|
|
712
675
|
recorded_by: historyRecord.recordedBy,
|
|
713
676
|
recorded_at: historyRecord.recordedAt,
|
|
714
|
-
...this.
|
|
677
|
+
...this.organizationId ? { organization_id: this.organizationId } : {},
|
|
678
|
+
...this.projectId !== void 0 ? { project_id: this.projectId } : {}
|
|
715
679
|
});
|
|
716
680
|
} catch (error) {
|
|
717
681
|
console.error(`Failed to create history record for ${type}/${name}:`, error);
|
|
@@ -743,7 +707,8 @@ var DatabaseLoader = class {
|
|
|
743
707
|
strategy: row.strategy ?? "merge",
|
|
744
708
|
owner: row.owner,
|
|
745
709
|
state: row.state ?? "active",
|
|
746
|
-
|
|
710
|
+
organizationId: row.organization_id,
|
|
711
|
+
projectId: row.project_id,
|
|
747
712
|
version: row.version ?? 1,
|
|
748
713
|
checksum: row.checksum,
|
|
749
714
|
source: row.source,
|
|
@@ -760,12 +725,24 @@ var DatabaseLoader = class {
|
|
|
760
725
|
async load(type, name, _options) {
|
|
761
726
|
const startTime = Date.now();
|
|
762
727
|
await this.ensureSchema();
|
|
728
|
+
const key = this.cacheKey(type, name);
|
|
729
|
+
if (this.loadCache) {
|
|
730
|
+
const cached = this.loadCache.get(key);
|
|
731
|
+
if (cached !== void 0) {
|
|
732
|
+
return {
|
|
733
|
+
data: cached,
|
|
734
|
+
source: "database",
|
|
735
|
+
format: "json",
|
|
736
|
+
loadTime: Date.now() - startTime
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
}
|
|
763
740
|
try {
|
|
764
|
-
const row = await this.
|
|
765
|
-
object: this.tableName,
|
|
741
|
+
const row = await this._findOne(this.tableName, {
|
|
766
742
|
where: this.baseFilter(type, name)
|
|
767
743
|
});
|
|
768
744
|
if (!row) {
|
|
745
|
+
this.loadCache?.set(key, null);
|
|
769
746
|
return {
|
|
770
747
|
data: null,
|
|
771
748
|
loadTime: Date.now() - startTime
|
|
@@ -773,6 +750,7 @@ var DatabaseLoader = class {
|
|
|
773
750
|
}
|
|
774
751
|
const data = this.rowToData(row);
|
|
775
752
|
const record = this.rowToRecord(row);
|
|
753
|
+
this.loadCache?.set(key, data);
|
|
776
754
|
return {
|
|
777
755
|
data,
|
|
778
756
|
source: "database",
|
|
@@ -789,21 +767,29 @@ var DatabaseLoader = class {
|
|
|
789
767
|
}
|
|
790
768
|
async loadMany(type, _options) {
|
|
791
769
|
await this.ensureSchema();
|
|
770
|
+
if (this.loadManyCache) {
|
|
771
|
+
const cached = this.loadManyCache.get(type);
|
|
772
|
+
if (cached !== void 0) return cached;
|
|
773
|
+
}
|
|
792
774
|
try {
|
|
793
|
-
const rows = await this.
|
|
794
|
-
object: this.tableName,
|
|
775
|
+
const rows = await this._find(this.tableName, {
|
|
795
776
|
where: this.baseFilter(type)
|
|
796
777
|
});
|
|
797
|
-
|
|
778
|
+
const result = rows.map((row) => this.rowToData(row)).filter((data) => data !== null);
|
|
779
|
+
this.loadManyCache?.set(type, result);
|
|
780
|
+
return result;
|
|
798
781
|
} catch {
|
|
799
782
|
return [];
|
|
800
783
|
}
|
|
801
784
|
}
|
|
802
785
|
async exists(type, name) {
|
|
803
786
|
await this.ensureSchema();
|
|
787
|
+
if (this.loadCache) {
|
|
788
|
+
const cached = this.loadCache.get(this.cacheKey(type, name));
|
|
789
|
+
if (cached !== void 0) return cached !== null;
|
|
790
|
+
}
|
|
804
791
|
try {
|
|
805
|
-
const count = await this.
|
|
806
|
-
object: this.tableName,
|
|
792
|
+
const count = await this._count(this.tableName, {
|
|
807
793
|
where: this.baseFilter(type, name)
|
|
808
794
|
});
|
|
809
795
|
return count > 0;
|
|
@@ -813,33 +799,47 @@ var DatabaseLoader = class {
|
|
|
813
799
|
}
|
|
814
800
|
async stat(type, name) {
|
|
815
801
|
await this.ensureSchema();
|
|
802
|
+
const key = this.cacheKey(type, name);
|
|
803
|
+
if (this.statCache) {
|
|
804
|
+
const cached = this.statCache.get(key);
|
|
805
|
+
if (cached !== void 0) return cached;
|
|
806
|
+
}
|
|
816
807
|
try {
|
|
817
|
-
const row = await this.
|
|
818
|
-
object: this.tableName,
|
|
808
|
+
const row = await this._findOne(this.tableName, {
|
|
819
809
|
where: this.baseFilter(type, name)
|
|
820
810
|
});
|
|
821
|
-
if (!row)
|
|
811
|
+
if (!row) {
|
|
812
|
+
this.statCache?.set(key, null);
|
|
813
|
+
return null;
|
|
814
|
+
}
|
|
822
815
|
const record = this.rowToRecord(row);
|
|
823
816
|
const metadataStr = typeof row.metadata === "string" ? row.metadata : JSON.stringify(row.metadata);
|
|
824
|
-
|
|
817
|
+
const stats = {
|
|
825
818
|
size: metadataStr.length,
|
|
826
819
|
mtime: record.updatedAt ?? record.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
827
820
|
format: "json",
|
|
828
821
|
etag: record.checksum
|
|
829
822
|
};
|
|
823
|
+
this.statCache?.set(key, stats);
|
|
824
|
+
return stats;
|
|
830
825
|
} catch {
|
|
831
826
|
return null;
|
|
832
827
|
}
|
|
833
828
|
}
|
|
834
829
|
async list(type) {
|
|
835
830
|
await this.ensureSchema();
|
|
831
|
+
if (this.listCache) {
|
|
832
|
+
const cached = this.listCache.get(type);
|
|
833
|
+
if (cached !== void 0) return cached;
|
|
834
|
+
}
|
|
836
835
|
try {
|
|
837
|
-
const rows = await this.
|
|
838
|
-
object: this.tableName,
|
|
836
|
+
const rows = await this._find(this.tableName, {
|
|
839
837
|
where: this.baseFilter(type),
|
|
840
838
|
fields: ["name"]
|
|
841
839
|
});
|
|
842
|
-
|
|
840
|
+
const names = rows.map((row) => row.name).filter((name) => typeof name === "string");
|
|
841
|
+
this.listCache?.set(type, names);
|
|
842
|
+
return names;
|
|
843
843
|
} catch {
|
|
844
844
|
return [];
|
|
845
845
|
}
|
|
@@ -851,8 +851,7 @@ var DatabaseLoader = class {
|
|
|
851
851
|
async getHistoryRecord(type, name, version) {
|
|
852
852
|
if (!this.trackHistory) return null;
|
|
853
853
|
await this.ensureHistorySchema();
|
|
854
|
-
const metadataRow = await this.
|
|
855
|
-
object: this.tableName,
|
|
854
|
+
const metadataRow = await this._findOne(this.tableName, {
|
|
856
855
|
where: this.baseFilter(type, name)
|
|
857
856
|
});
|
|
858
857
|
if (!metadataRow) return null;
|
|
@@ -860,11 +859,11 @@ var DatabaseLoader = class {
|
|
|
860
859
|
metadata_id: metadataRow.id,
|
|
861
860
|
version
|
|
862
861
|
};
|
|
863
|
-
if (this.
|
|
864
|
-
filter.
|
|
862
|
+
if (this.organizationId) {
|
|
863
|
+
filter.organization_id = this.organizationId;
|
|
865
864
|
}
|
|
866
|
-
|
|
867
|
-
|
|
865
|
+
filter.project_id = this.projectId ?? null;
|
|
866
|
+
const row = await this._findOne(this.historyTableName, {
|
|
868
867
|
where: filter
|
|
869
868
|
});
|
|
870
869
|
if (!row) return null;
|
|
@@ -879,11 +878,80 @@ var DatabaseLoader = class {
|
|
|
879
878
|
checksum: row.checksum,
|
|
880
879
|
previousChecksum: row.previous_checksum,
|
|
881
880
|
changeNote: row.change_note,
|
|
882
|
-
|
|
881
|
+
organizationId: row.organization_id,
|
|
882
|
+
projectId: row.project_id,
|
|
883
883
|
recordedBy: row.recorded_by,
|
|
884
884
|
recordedAt: row.recorded_at
|
|
885
885
|
};
|
|
886
886
|
}
|
|
887
|
+
/**
|
|
888
|
+
* Query history records with pagination and filtering.
|
|
889
|
+
* Encapsulates history table queries so MetadataManager doesn't need
|
|
890
|
+
* direct driver access.
|
|
891
|
+
*/
|
|
892
|
+
async queryHistory(type, name, options) {
|
|
893
|
+
if (!this.trackHistory) {
|
|
894
|
+
return { records: [], total: 0, hasMore: false };
|
|
895
|
+
}
|
|
896
|
+
await this.ensureSchema();
|
|
897
|
+
await this.ensureHistorySchema();
|
|
898
|
+
const filter = { type, name };
|
|
899
|
+
if (this.organizationId) filter.organization_id = this.organizationId;
|
|
900
|
+
filter.project_id = this.projectId ?? null;
|
|
901
|
+
const metadataRecord = await this._findOne(this.tableName, { where: filter });
|
|
902
|
+
if (!metadataRecord) {
|
|
903
|
+
return { records: [], total: 0, hasMore: false };
|
|
904
|
+
}
|
|
905
|
+
const historyFilter = {
|
|
906
|
+
metadata_id: metadataRecord.id
|
|
907
|
+
};
|
|
908
|
+
if (this.organizationId) historyFilter.organization_id = this.organizationId;
|
|
909
|
+
historyFilter.project_id = this.projectId ?? null;
|
|
910
|
+
if (options?.operationType) historyFilter.operation_type = options.operationType;
|
|
911
|
+
if (options?.since) historyFilter.recorded_at = { $gte: options.since };
|
|
912
|
+
if (options?.until) {
|
|
913
|
+
if (historyFilter.recorded_at) {
|
|
914
|
+
historyFilter.recorded_at.$lte = options.until;
|
|
915
|
+
} else {
|
|
916
|
+
historyFilter.recorded_at = { $lte: options.until };
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
const limit = options?.limit ?? 50;
|
|
920
|
+
const offset = options?.offset ?? 0;
|
|
921
|
+
const historyRecords = await this._find(this.historyTableName, {
|
|
922
|
+
where: historyFilter,
|
|
923
|
+
orderBy: [
|
|
924
|
+
{ field: "recorded_at", order: "desc" },
|
|
925
|
+
{ field: "version", order: "desc" }
|
|
926
|
+
],
|
|
927
|
+
limit: limit + 1,
|
|
928
|
+
offset
|
|
929
|
+
});
|
|
930
|
+
const hasMore = historyRecords.length > limit;
|
|
931
|
+
const records = historyRecords.slice(0, limit);
|
|
932
|
+
const total = await this._count(this.historyTableName, { where: historyFilter });
|
|
933
|
+
const includeMetadata = options?.includeMetadata !== false;
|
|
934
|
+
const result = records.map((row) => {
|
|
935
|
+
const parsedMetadata = typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata;
|
|
936
|
+
return {
|
|
937
|
+
id: row.id,
|
|
938
|
+
metadataId: row.metadata_id,
|
|
939
|
+
name: row.name,
|
|
940
|
+
type: row.type,
|
|
941
|
+
version: row.version,
|
|
942
|
+
operationType: row.operation_type,
|
|
943
|
+
metadata: includeMetadata ? parsedMetadata : null,
|
|
944
|
+
checksum: row.checksum,
|
|
945
|
+
previousChecksum: row.previous_checksum,
|
|
946
|
+
changeNote: row.change_note,
|
|
947
|
+
organizationId: row.organization_id,
|
|
948
|
+
projectId: row.project_id,
|
|
949
|
+
recordedBy: row.recorded_by,
|
|
950
|
+
recordedAt: row.recorded_at
|
|
951
|
+
};
|
|
952
|
+
});
|
|
953
|
+
return { records: result, total, hasMore };
|
|
954
|
+
}
|
|
887
955
|
/**
|
|
888
956
|
* Perform a rollback: persist `restoredData` as the new current state and record a
|
|
889
957
|
* single 'revert' history entry (instead of the usual 'update' entry that `save()`
|
|
@@ -896,8 +964,7 @@ var DatabaseLoader = class {
|
|
|
896
964
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
897
965
|
const metadataJson = JSON.stringify(restoredData);
|
|
898
966
|
const newChecksum = await calculateChecksum(restoredData);
|
|
899
|
-
const existing = await this.
|
|
900
|
-
object: this.tableName,
|
|
967
|
+
const existing = await this._findOne(this.tableName, {
|
|
901
968
|
where: this.baseFilter(type, name)
|
|
902
969
|
});
|
|
903
970
|
if (!existing) {
|
|
@@ -905,13 +972,14 @@ var DatabaseLoader = class {
|
|
|
905
972
|
}
|
|
906
973
|
const previousChecksum = existing.checksum;
|
|
907
974
|
const newVersion = (existing.version ?? 0) + 1;
|
|
908
|
-
await this.
|
|
975
|
+
await this._update(this.tableName, existing.id, {
|
|
909
976
|
metadata: metadataJson,
|
|
910
977
|
version: newVersion,
|
|
911
978
|
checksum: newChecksum,
|
|
912
979
|
updated_at: now,
|
|
913
980
|
state: "active"
|
|
914
981
|
});
|
|
982
|
+
this.invalidate(type, name);
|
|
915
983
|
await this.createHistoryRecord(
|
|
916
984
|
existing.id,
|
|
917
985
|
type,
|
|
@@ -931,13 +999,13 @@ var DatabaseLoader = class {
|
|
|
931
999
|
const metadataJson = JSON.stringify(data);
|
|
932
1000
|
const newChecksum = await calculateChecksum(data);
|
|
933
1001
|
try {
|
|
934
|
-
const existing = await this.
|
|
935
|
-
object: this.tableName,
|
|
1002
|
+
const existing = await this._findOne(this.tableName, {
|
|
936
1003
|
where: this.baseFilter(type, name)
|
|
937
1004
|
});
|
|
938
1005
|
if (existing) {
|
|
939
1006
|
const previousChecksum = existing.checksum;
|
|
940
1007
|
if (newChecksum === previousChecksum) {
|
|
1008
|
+
this.loadCache?.set(this.cacheKey(type, name), data);
|
|
941
1009
|
return {
|
|
942
1010
|
success: true,
|
|
943
1011
|
path: `datasource://${this.tableName}/${type}/${name}`,
|
|
@@ -946,13 +1014,14 @@ var DatabaseLoader = class {
|
|
|
946
1014
|
};
|
|
947
1015
|
}
|
|
948
1016
|
const version = (existing.version ?? 0) + 1;
|
|
949
|
-
await this.
|
|
1017
|
+
await this._update(this.tableName, existing.id, {
|
|
950
1018
|
metadata: metadataJson,
|
|
951
1019
|
version,
|
|
952
1020
|
checksum: newChecksum,
|
|
953
1021
|
updated_at: now,
|
|
954
1022
|
state: "active"
|
|
955
1023
|
});
|
|
1024
|
+
this.invalidate(type, name);
|
|
956
1025
|
await this.createHistoryRecord(
|
|
957
1026
|
existing.id,
|
|
958
1027
|
type,
|
|
@@ -970,7 +1039,7 @@ var DatabaseLoader = class {
|
|
|
970
1039
|
};
|
|
971
1040
|
} else {
|
|
972
1041
|
const id = generateId();
|
|
973
|
-
await this.
|
|
1042
|
+
await this._create(this.tableName, {
|
|
974
1043
|
id,
|
|
975
1044
|
name,
|
|
976
1045
|
type,
|
|
@@ -982,10 +1051,12 @@ var DatabaseLoader = class {
|
|
|
982
1051
|
state: "active",
|
|
983
1052
|
version: 1,
|
|
984
1053
|
source: "database",
|
|
985
|
-
...this.
|
|
1054
|
+
...this.organizationId ? { organization_id: this.organizationId } : {},
|
|
1055
|
+
...this.projectId !== void 0 ? { project_id: this.projectId } : { project_id: null },
|
|
986
1056
|
created_at: now,
|
|
987
1057
|
updated_at: now
|
|
988
1058
|
});
|
|
1059
|
+
this.invalidate(type, name);
|
|
989
1060
|
await this.createHistoryRecord(
|
|
990
1061
|
id,
|
|
991
1062
|
type,
|
|
@@ -1012,14 +1083,14 @@ var DatabaseLoader = class {
|
|
|
1012
1083
|
*/
|
|
1013
1084
|
async delete(type, name) {
|
|
1014
1085
|
await this.ensureSchema();
|
|
1015
|
-
const existing = await this.
|
|
1016
|
-
object: this.tableName,
|
|
1086
|
+
const existing = await this._findOne(this.tableName, {
|
|
1017
1087
|
where: this.baseFilter(type, name)
|
|
1018
1088
|
});
|
|
1019
1089
|
if (!existing) {
|
|
1020
1090
|
return;
|
|
1021
1091
|
}
|
|
1022
|
-
await this.
|
|
1092
|
+
await this._delete(this.tableName, existing.id);
|
|
1093
|
+
this.invalidate(type, name);
|
|
1023
1094
|
}
|
|
1024
1095
|
};
|
|
1025
1096
|
function generateId() {
|
|
@@ -1030,7 +1101,7 @@ function generateId() {
|
|
|
1030
1101
|
}
|
|
1031
1102
|
|
|
1032
1103
|
// src/metadata-manager.ts
|
|
1033
|
-
var
|
|
1104
|
+
var _MetadataManager = class _MetadataManager {
|
|
1034
1105
|
constructor(config) {
|
|
1035
1106
|
this.loaders = /* @__PURE__ */ new Map();
|
|
1036
1107
|
this.watchCallbacks = /* @__PURE__ */ new Map();
|
|
@@ -1042,6 +1113,18 @@ var MetadataManager = class {
|
|
|
1042
1113
|
this.typeRegistry = [];
|
|
1043
1114
|
// Dependency tracking: "type:name" -> dependencies
|
|
1044
1115
|
this.dependencies = /* @__PURE__ */ new Map();
|
|
1116
|
+
// Short-lived cache for list() results. Built primarily to break the
|
|
1117
|
+
// deadlock that occurs when security/permission middleware calls
|
|
1118
|
+
// `list('permission')` from inside a user-initiated DB transaction: the
|
|
1119
|
+
// DatabaseLoader's `engine.find('sys_metadata', ...)` would then try to
|
|
1120
|
+
// acquire a fresh knex connection while the transaction is still holding
|
|
1121
|
+
// SQLite's single connection — knex waits the full `acquireConnectionTimeout`
|
|
1122
|
+
// (60s) before returning []. The cache absorbs the repeated lookups so the
|
|
1123
|
+
// loader is only hit once per TTL window.
|
|
1124
|
+
//
|
|
1125
|
+
// Invalidated on every `register()` / `unregister()` to keep CRUD writes
|
|
1126
|
+
// visible to subsequent reads.
|
|
1127
|
+
this.listCache = /* @__PURE__ */ new Map();
|
|
1045
1128
|
this.config = config;
|
|
1046
1129
|
this.logger = (0, import_core.createLogger)({ level: "info", format: "pretty" });
|
|
1047
1130
|
this.serializers = /* @__PURE__ */ new Map();
|
|
@@ -1076,16 +1159,57 @@ var MetadataManager = class {
|
|
|
1076
1159
|
* Can be called at any time to enable database storage (e.g. after kernel resolves the driver).
|
|
1077
1160
|
*
|
|
1078
1161
|
* @param driver - An IDataDriver instance for database operations
|
|
1079
|
-
|
|
1080
|
-
|
|
1162
|
+
* @param organizationId - Organization ID for multi-tenant isolation
|
|
1163
|
+
* @param projectId - Project ID (undefined = platform-global)
|
|
1164
|
+
*/
|
|
1165
|
+
setDatabaseDriver(driver, organizationId, projectId) {
|
|
1166
|
+
if (projectId !== void 0) {
|
|
1167
|
+
this.logger.info("Project kernel \u2014 skipping DatabaseLoader for sys_metadata (control-plane only)", {
|
|
1168
|
+
organizationId,
|
|
1169
|
+
projectId
|
|
1170
|
+
});
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1081
1173
|
const tableName = this.config.tableName ?? "sys_metadata";
|
|
1082
1174
|
const dbLoader = new DatabaseLoader({
|
|
1083
1175
|
driver,
|
|
1084
|
-
tableName
|
|
1176
|
+
tableName,
|
|
1177
|
+
organizationId,
|
|
1178
|
+
projectId,
|
|
1179
|
+
cache: this.config.cache?.databaseLoader
|
|
1085
1180
|
});
|
|
1086
1181
|
this.registerLoader(dbLoader);
|
|
1087
1182
|
this.logger.info("DatabaseLoader configured", { datasource: this.config.datasource, tableName });
|
|
1088
1183
|
}
|
|
1184
|
+
/**
|
|
1185
|
+
* Configure and register a DatabaseLoader backed by an IDataEngine (ObjectQL).
|
|
1186
|
+
* The engine handles datasource routing automatically — sys_metadata will
|
|
1187
|
+
* be routed to the correct driver via the standard namespace mapping.
|
|
1188
|
+
* No manual driver resolution needed.
|
|
1189
|
+
*
|
|
1190
|
+
* @param engine - An IDataEngine instance (typically the ObjectQL service)
|
|
1191
|
+
* @param organizationId - Organization ID for multi-tenant isolation
|
|
1192
|
+
* @param projectId - Project ID (undefined = platform-global)
|
|
1193
|
+
*/
|
|
1194
|
+
setDataEngine(engine, organizationId, projectId) {
|
|
1195
|
+
if (projectId !== void 0) {
|
|
1196
|
+
this.logger.info("Project kernel \u2014 skipping DatabaseLoader for sys_metadata (control-plane only)", {
|
|
1197
|
+
organizationId,
|
|
1198
|
+
projectId
|
|
1199
|
+
});
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
const tableName = this.config.tableName ?? "sys_metadata";
|
|
1203
|
+
const dbLoader = new DatabaseLoader({
|
|
1204
|
+
engine,
|
|
1205
|
+
tableName,
|
|
1206
|
+
organizationId,
|
|
1207
|
+
projectId,
|
|
1208
|
+
cache: this.config.cache?.databaseLoader
|
|
1209
|
+
});
|
|
1210
|
+
this.registerLoader(dbLoader);
|
|
1211
|
+
this.logger.info("DatabaseLoader configured via DataEngine", { tableName });
|
|
1212
|
+
}
|
|
1089
1213
|
/**
|
|
1090
1214
|
* Set the realtime service for publishing metadata change events.
|
|
1091
1215
|
* Should be called after kernel resolves the realtime service.
|
|
@@ -1113,10 +1237,19 @@ var MetadataManager = class {
|
|
|
1113
1237
|
* should not be written to during runtime registration.
|
|
1114
1238
|
*/
|
|
1115
1239
|
async register(type, name, data) {
|
|
1240
|
+
if (this.config.persistence?.writable === false) {
|
|
1241
|
+
const msg = `MetadataManager is read-only (persistence.writable=false); refusing to register ${type}/${name}`;
|
|
1242
|
+
if (this.config.validation?.throwOnError) {
|
|
1243
|
+
throw new Error(msg);
|
|
1244
|
+
}
|
|
1245
|
+
this.logger.warn(msg);
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1116
1248
|
if (!this.registry.has(type)) {
|
|
1117
1249
|
this.registry.set(type, /* @__PURE__ */ new Map());
|
|
1118
1250
|
}
|
|
1119
1251
|
this.registry.get(type).set(name, data);
|
|
1252
|
+
this.invalidateListCache(type);
|
|
1120
1253
|
for (const loader of this.loaders.values()) {
|
|
1121
1254
|
if (loader.save && loader.contract.protocol === "datasource:" && loader.contract.capabilities.write) {
|
|
1122
1255
|
await loader.save(type, name, data);
|
|
@@ -1158,6 +1291,10 @@ var MetadataManager = class {
|
|
|
1158
1291
|
* List all metadata items of a given type
|
|
1159
1292
|
*/
|
|
1160
1293
|
async list(type) {
|
|
1294
|
+
const cached = this.listCache.get(type);
|
|
1295
|
+
if (cached && Date.now() - cached.ts < _MetadataManager.LIST_CACHE_TTL_MS) {
|
|
1296
|
+
return cached.items;
|
|
1297
|
+
}
|
|
1161
1298
|
const items = /* @__PURE__ */ new Map();
|
|
1162
1299
|
const typeStore = this.registry.get(type);
|
|
1163
1300
|
if (typeStore) {
|
|
@@ -1178,7 +1315,16 @@ var MetadataManager = class {
|
|
|
1178
1315
|
this.logger.warn(`Loader ${loader.contract.name} failed to loadMany ${type}`, { error: e });
|
|
1179
1316
|
}
|
|
1180
1317
|
}
|
|
1181
|
-
|
|
1318
|
+
const result = Array.from(items.values());
|
|
1319
|
+
this.cacheListResult(type, result);
|
|
1320
|
+
return result;
|
|
1321
|
+
}
|
|
1322
|
+
cacheListResult(type, items) {
|
|
1323
|
+
this.listCache.set(type, { ts: Date.now(), items });
|
|
1324
|
+
}
|
|
1325
|
+
/** Internal helper: drop the cached `list()` result for a type. */
|
|
1326
|
+
invalidateListCache(type) {
|
|
1327
|
+
this.listCache.delete(type);
|
|
1182
1328
|
}
|
|
1183
1329
|
/**
|
|
1184
1330
|
* Unregister/remove a metadata item by type and name.
|
|
@@ -1192,6 +1338,7 @@ var MetadataManager = class {
|
|
|
1192
1338
|
this.registry.delete(type);
|
|
1193
1339
|
}
|
|
1194
1340
|
}
|
|
1341
|
+
this.invalidateListCache(type);
|
|
1195
1342
|
for (const loader of this.loaders.values()) {
|
|
1196
1343
|
if (loader.contract.protocol !== "datasource:" || !loader.contract.capabilities.write) continue;
|
|
1197
1344
|
if (typeof loader.delete === "function") {
|
|
@@ -1606,6 +1753,14 @@ var MetadataManager = class {
|
|
|
1606
1753
|
* Save/update an overlay for a metadata item
|
|
1607
1754
|
*/
|
|
1608
1755
|
async saveOverlay(overlay) {
|
|
1756
|
+
if (this.config.persistence?.overlayWritable === false) {
|
|
1757
|
+
const msg = `MetadataManager overlays are read-only (persistence.overlayWritable=false); refusing to save overlay for ${overlay.baseType}/${overlay.baseName}`;
|
|
1758
|
+
if (this.config.validation?.throwOnError) {
|
|
1759
|
+
throw new Error(msg);
|
|
1760
|
+
}
|
|
1761
|
+
this.logger.warn(msg);
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1609
1764
|
const key = this.overlayKey(overlay.baseType, overlay.baseName, overlay.scope);
|
|
1610
1765
|
this.overlays.set(key, overlay);
|
|
1611
1766
|
}
|
|
@@ -1999,84 +2154,14 @@ var MetadataManager = class {
|
|
|
1999
2154
|
if (!dbLoader) {
|
|
2000
2155
|
throw new Error("History tracking requires a database loader to be configured");
|
|
2001
2156
|
}
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
}
|
|
2010
|
-
const metadataRecord = await driver.findOne(tableName, {
|
|
2011
|
-
object: tableName,
|
|
2012
|
-
where: filter
|
|
2013
|
-
});
|
|
2014
|
-
if (!metadataRecord) {
|
|
2015
|
-
return {
|
|
2016
|
-
records: [],
|
|
2017
|
-
total: 0,
|
|
2018
|
-
hasMore: false
|
|
2019
|
-
};
|
|
2020
|
-
}
|
|
2021
|
-
const historyFilter = {
|
|
2022
|
-
metadata_id: metadataRecord.id
|
|
2023
|
-
};
|
|
2024
|
-
if (tenantId) {
|
|
2025
|
-
historyFilter.tenant_id = tenantId;
|
|
2026
|
-
}
|
|
2027
|
-
if (options?.operationType) {
|
|
2028
|
-
historyFilter.operation_type = options.operationType;
|
|
2029
|
-
}
|
|
2030
|
-
if (options?.since) {
|
|
2031
|
-
historyFilter.recorded_at = { $gte: options.since };
|
|
2032
|
-
}
|
|
2033
|
-
if (options?.until) {
|
|
2034
|
-
if (historyFilter.recorded_at) {
|
|
2035
|
-
historyFilter.recorded_at.$lte = options.until;
|
|
2036
|
-
} else {
|
|
2037
|
-
historyFilter.recorded_at = { $lte: options.until };
|
|
2038
|
-
}
|
|
2039
|
-
}
|
|
2040
|
-
const limit = options?.limit ?? 50;
|
|
2041
|
-
const offset = options?.offset ?? 0;
|
|
2042
|
-
const historyRecords = await driver.find(historyTableName, {
|
|
2043
|
-
object: historyTableName,
|
|
2044
|
-
where: historyFilter,
|
|
2045
|
-
orderBy: [{ field: "recorded_at", order: "desc" }],
|
|
2046
|
-
limit: limit + 1,
|
|
2047
|
-
// Fetch one extra to determine hasMore
|
|
2048
|
-
offset
|
|
2049
|
-
});
|
|
2050
|
-
const hasMore = historyRecords.length > limit;
|
|
2051
|
-
const records = historyRecords.slice(0, limit);
|
|
2052
|
-
const total = await driver.count(historyTableName, {
|
|
2053
|
-
object: historyTableName,
|
|
2054
|
-
where: historyFilter
|
|
2055
|
-
});
|
|
2056
|
-
const includeMetadata = options?.includeMetadata !== false;
|
|
2057
|
-
const historyResult = records.map((row) => {
|
|
2058
|
-
const parsedMetadata = typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata;
|
|
2059
|
-
return {
|
|
2060
|
-
id: row.id,
|
|
2061
|
-
metadataId: row.metadata_id,
|
|
2062
|
-
name: row.name,
|
|
2063
|
-
type: row.type,
|
|
2064
|
-
version: row.version,
|
|
2065
|
-
operationType: row.operation_type,
|
|
2066
|
-
metadata: includeMetadata ? parsedMetadata : null,
|
|
2067
|
-
checksum: row.checksum,
|
|
2068
|
-
previousChecksum: row.previous_checksum,
|
|
2069
|
-
changeNote: row.change_note,
|
|
2070
|
-
tenantId: row.tenant_id,
|
|
2071
|
-
recordedBy: row.recorded_by,
|
|
2072
|
-
recordedAt: row.recorded_at
|
|
2073
|
-
};
|
|
2157
|
+
return dbLoader.queryHistory(type, name, {
|
|
2158
|
+
operationType: options?.operationType,
|
|
2159
|
+
since: options?.since,
|
|
2160
|
+
until: options?.until,
|
|
2161
|
+
limit: options?.limit,
|
|
2162
|
+
offset: options?.offset,
|
|
2163
|
+
includeMetadata: options?.includeMetadata
|
|
2074
2164
|
});
|
|
2075
|
-
return {
|
|
2076
|
-
records: historyResult,
|
|
2077
|
-
total,
|
|
2078
|
-
hasMore
|
|
2079
|
-
};
|
|
2080
2165
|
}
|
|
2081
2166
|
/**
|
|
2082
2167
|
* Rollback a metadata item to a specific version.
|
|
@@ -2145,6 +2230,12 @@ var MetadataManager = class {
|
|
|
2145
2230
|
};
|
|
2146
2231
|
}
|
|
2147
2232
|
};
|
|
2233
|
+
_MetadataManager.LIST_CACHE_TTL_MS = 3e4;
|
|
2234
|
+
var MetadataManager = _MetadataManager;
|
|
2235
|
+
|
|
2236
|
+
// src/plugin.ts
|
|
2237
|
+
var import_promises = require("fs/promises");
|
|
2238
|
+
var import_node_crypto2 = require("crypto");
|
|
2148
2239
|
|
|
2149
2240
|
// src/node-metadata-manager.ts
|
|
2150
2241
|
var path2 = __toESM(require("path"), 1);
|
|
@@ -2262,7 +2353,7 @@ var FilesystemLoader = class {
|
|
|
2262
2353
|
);
|
|
2263
2354
|
for (const pattern of globPatterns) {
|
|
2264
2355
|
const files = await (0, import_glob.glob)(pattern, {
|
|
2265
|
-
ignore: ["**/node_modules/**", "**/*.test.*", "**/*.spec.*"],
|
|
2356
|
+
ignore: ["**/node_modules/**", "**/*.test.*", "**/*.spec.*", "**/*[*]*"],
|
|
2266
2357
|
nodir: true
|
|
2267
2358
|
});
|
|
2268
2359
|
for (const file of files) {
|
|
@@ -2551,124 +2642,6 @@ var NodeMetadataManager = class extends MetadataManager {
|
|
|
2551
2642
|
}
|
|
2552
2643
|
};
|
|
2553
2644
|
|
|
2554
|
-
// src/plugin.ts
|
|
2555
|
-
var import_kernel = require("@objectstack/spec/kernel");
|
|
2556
|
-
var MetadataPlugin = class {
|
|
2557
|
-
constructor(options = {}) {
|
|
2558
|
-
this.name = "com.objectstack.metadata";
|
|
2559
|
-
this.type = "standard";
|
|
2560
|
-
this.version = "1.0.0";
|
|
2561
|
-
this.init = async (ctx) => {
|
|
2562
|
-
ctx.logger.info("Initializing Metadata Manager", {
|
|
2563
|
-
root: this.options.rootDir || process.cwd(),
|
|
2564
|
-
watch: this.options.watch
|
|
2565
|
-
});
|
|
2566
|
-
ctx.registerService("metadata", this.manager);
|
|
2567
|
-
console.log("[MetadataPlugin] Registered metadata service, has getRegisteredTypes:", typeof this.manager.getRegisteredTypes);
|
|
2568
|
-
try {
|
|
2569
|
-
ctx.getService("manifest").register({
|
|
2570
|
-
id: "com.objectstack.metadata",
|
|
2571
|
-
name: "Metadata",
|
|
2572
|
-
version: "1.0.0",
|
|
2573
|
-
type: "plugin",
|
|
2574
|
-
namespace: "sys",
|
|
2575
|
-
objects: [SysMetadataObject]
|
|
2576
|
-
});
|
|
2577
|
-
} catch {
|
|
2578
|
-
}
|
|
2579
|
-
ctx.logger.info("MetadataPlugin providing metadata service (primary mode)", {
|
|
2580
|
-
mode: "file-system",
|
|
2581
|
-
features: ["watch", "persistence", "multi-format", "query", "overlay", "type-registry"]
|
|
2582
|
-
});
|
|
2583
|
-
};
|
|
2584
|
-
this.start = async (ctx) => {
|
|
2585
|
-
ctx.logger.info("Loading metadata from file system...");
|
|
2586
|
-
const sortedTypes = [...import_kernel.DEFAULT_METADATA_TYPE_REGISTRY].sort((a, b) => a.loadOrder - b.loadOrder);
|
|
2587
|
-
let totalLoaded = 0;
|
|
2588
|
-
for (const entry of sortedTypes) {
|
|
2589
|
-
try {
|
|
2590
|
-
const items = await this.manager.loadMany(entry.type, {
|
|
2591
|
-
recursive: true
|
|
2592
|
-
});
|
|
2593
|
-
if (items.length > 0) {
|
|
2594
|
-
for (const item of items) {
|
|
2595
|
-
const meta = item;
|
|
2596
|
-
if (meta?.name) {
|
|
2597
|
-
await this.manager.register(entry.type, meta.name, item);
|
|
2598
|
-
}
|
|
2599
|
-
}
|
|
2600
|
-
ctx.logger.info(`Loaded ${items.length} ${entry.type} from file system`);
|
|
2601
|
-
totalLoaded += items.length;
|
|
2602
|
-
}
|
|
2603
|
-
} catch (e) {
|
|
2604
|
-
ctx.logger.debug(`No ${entry.type} metadata found`, { error: e.message });
|
|
2605
|
-
}
|
|
2606
|
-
}
|
|
2607
|
-
ctx.logger.info("Metadata loading complete", {
|
|
2608
|
-
totalItems: totalLoaded,
|
|
2609
|
-
registeredTypes: sortedTypes.length
|
|
2610
|
-
});
|
|
2611
|
-
let driverBridged = false;
|
|
2612
|
-
try {
|
|
2613
|
-
const ql = ctx.getService("objectql");
|
|
2614
|
-
if (ql) {
|
|
2615
|
-
const tableName = this.manager["config"]?.tableName ?? "sys_metadata";
|
|
2616
|
-
const driver = ql.getDriverForObject?.(tableName);
|
|
2617
|
-
if (driver) {
|
|
2618
|
-
ctx.logger.info("[MetadataPlugin] Bridging driver to MetadataManager via ObjectQL routing", {
|
|
2619
|
-
tableName,
|
|
2620
|
-
driver: driver.name
|
|
2621
|
-
});
|
|
2622
|
-
this.manager.setDatabaseDriver(driver);
|
|
2623
|
-
driverBridged = true;
|
|
2624
|
-
} else {
|
|
2625
|
-
ctx.logger.debug("[MetadataPlugin] ObjectQL could not resolve driver for metadata table", { tableName });
|
|
2626
|
-
}
|
|
2627
|
-
}
|
|
2628
|
-
} catch {
|
|
2629
|
-
}
|
|
2630
|
-
if (!driverBridged) {
|
|
2631
|
-
try {
|
|
2632
|
-
const services = ctx.getServices();
|
|
2633
|
-
for (const [serviceName, service] of services) {
|
|
2634
|
-
if (serviceName.startsWith("driver.") && service) {
|
|
2635
|
-
ctx.logger.info("[MetadataPlugin] Bridging driver to MetadataManager (fallback: first driver)", {
|
|
2636
|
-
driverService: serviceName
|
|
2637
|
-
});
|
|
2638
|
-
this.manager.setDatabaseDriver(service);
|
|
2639
|
-
break;
|
|
2640
|
-
}
|
|
2641
|
-
}
|
|
2642
|
-
} catch (e) {
|
|
2643
|
-
ctx.logger.debug("[MetadataPlugin] No driver service found", { error: e.message });
|
|
2644
|
-
}
|
|
2645
|
-
}
|
|
2646
|
-
try {
|
|
2647
|
-
const realtimeService = ctx.getService("realtime");
|
|
2648
|
-
if (realtimeService && typeof realtimeService === "object" && "publish" in realtimeService) {
|
|
2649
|
-
ctx.logger.info("[MetadataPlugin] Bridging realtime service to MetadataManager for event publishing");
|
|
2650
|
-
this.manager.setRealtimeService(realtimeService);
|
|
2651
|
-
}
|
|
2652
|
-
} catch (e) {
|
|
2653
|
-
ctx.logger.debug("[MetadataPlugin] No realtime service found \u2014 metadata events will not be published", {
|
|
2654
|
-
error: e.message
|
|
2655
|
-
});
|
|
2656
|
-
}
|
|
2657
|
-
};
|
|
2658
|
-
this.options = {
|
|
2659
|
-
watch: true,
|
|
2660
|
-
...options
|
|
2661
|
-
};
|
|
2662
|
-
const rootDir = this.options.rootDir || process.cwd();
|
|
2663
|
-
this.manager = new NodeMetadataManager({
|
|
2664
|
-
rootDir,
|
|
2665
|
-
watch: this.options.watch ?? true,
|
|
2666
|
-
formats: ["yaml", "json", "typescript", "javascript"]
|
|
2667
|
-
});
|
|
2668
|
-
this.manager.setTypeRegistry(import_kernel.DEFAULT_METADATA_TYPE_REGISTRY);
|
|
2669
|
-
}
|
|
2670
|
-
};
|
|
2671
|
-
|
|
2672
2645
|
// src/loaders/memory-loader.ts
|
|
2673
2646
|
var MemoryLoader = class {
|
|
2674
2647
|
constructor() {
|
|
@@ -2747,6 +2720,286 @@ var MemoryLoader = class {
|
|
|
2747
2720
|
}
|
|
2748
2721
|
};
|
|
2749
2722
|
|
|
2723
|
+
// src/plugin.ts
|
|
2724
|
+
var import_kernel = require("@objectstack/spec/kernel");
|
|
2725
|
+
var import_metadata2 = require("@objectstack/platform-objects/metadata");
|
|
2726
|
+
var queryableMetadataObjects = [
|
|
2727
|
+
import_metadata2.SysMetadataObject,
|
|
2728
|
+
import_metadata2.SysMetadataHistoryObject
|
|
2729
|
+
];
|
|
2730
|
+
var ARTIFACT_FIELD_TO_TYPE = {
|
|
2731
|
+
objects: "object",
|
|
2732
|
+
objectExtensions: "object_extension",
|
|
2733
|
+
apps: "app",
|
|
2734
|
+
views: "view",
|
|
2735
|
+
pages: "page",
|
|
2736
|
+
dashboards: "dashboard",
|
|
2737
|
+
reports: "report",
|
|
2738
|
+
actions: "action",
|
|
2739
|
+
themes: "theme",
|
|
2740
|
+
workflows: "workflow",
|
|
2741
|
+
approvals: "approval",
|
|
2742
|
+
flows: "flow",
|
|
2743
|
+
roles: "role",
|
|
2744
|
+
permissions: "permission",
|
|
2745
|
+
sharingRules: "sharing_rule",
|
|
2746
|
+
policies: "policy",
|
|
2747
|
+
apis: "api",
|
|
2748
|
+
webhooks: "webhook",
|
|
2749
|
+
agents: "agent",
|
|
2750
|
+
skills: "skill",
|
|
2751
|
+
ragPipelines: "rag_pipeline",
|
|
2752
|
+
hooks: "hook",
|
|
2753
|
+
mappings: "mapping",
|
|
2754
|
+
analyticsCubes: "analytics_cube",
|
|
2755
|
+
connectors: "connector",
|
|
2756
|
+
data: "dataset"
|
|
2757
|
+
};
|
|
2758
|
+
var MetadataPlugin = class {
|
|
2759
|
+
constructor(options = {}) {
|
|
2760
|
+
this.name = "com.objectstack.metadata";
|
|
2761
|
+
this.type = "standard";
|
|
2762
|
+
this.version = "1.0.0";
|
|
2763
|
+
this.init = async (ctx) => {
|
|
2764
|
+
ctx.logger.info("Initializing Metadata Manager", {
|
|
2765
|
+
root: this.options.rootDir || process.cwd(),
|
|
2766
|
+
watch: this.options.watch,
|
|
2767
|
+
artifactSource: this.options.artifactSource?.mode
|
|
2768
|
+
});
|
|
2769
|
+
ctx.registerService("metadata", this.manager);
|
|
2770
|
+
console.log("[MetadataPlugin] Registered metadata service, has getRegisteredTypes:", typeof this.manager.getRegisteredTypes);
|
|
2771
|
+
const registerSysObjects = this.options.registerSystemObjects !== false;
|
|
2772
|
+
if (registerSysObjects) {
|
|
2773
|
+
try {
|
|
2774
|
+
const manifestService = ctx.getService("manifest");
|
|
2775
|
+
manifestService.register({
|
|
2776
|
+
id: "com.objectstack.metadata-objects",
|
|
2777
|
+
name: "Metadata Platform Objects",
|
|
2778
|
+
version: "1.0.0",
|
|
2779
|
+
type: "plugin",
|
|
2780
|
+
scope: "system",
|
|
2781
|
+
defaultDatasource: "cloud",
|
|
2782
|
+
objects: queryableMetadataObjects
|
|
2783
|
+
});
|
|
2784
|
+
ctx.logger.info("Registered system metadata objects", {
|
|
2785
|
+
queryable: queryableMetadataObjects.map((object) => object.name)
|
|
2786
|
+
});
|
|
2787
|
+
} catch {
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
ctx.logger.info("MetadataPlugin providing metadata service (primary mode)", {
|
|
2791
|
+
mode: this.options.artifactSource?.mode ?? "file-system",
|
|
2792
|
+
features: ["watch", "multi-format", "query", "overlay", "type-registry"]
|
|
2793
|
+
});
|
|
2794
|
+
};
|
|
2795
|
+
this.start = async (ctx) => {
|
|
2796
|
+
const src = this.options.artifactSource;
|
|
2797
|
+
const mode = this.options.config?.bootstrap ?? "eager";
|
|
2798
|
+
ctx.logger.info("[MetadataPlugin] Bootstrapping metadata", {
|
|
2799
|
+
bootstrap: mode,
|
|
2800
|
+
artifactSource: src?.mode ?? "none"
|
|
2801
|
+
});
|
|
2802
|
+
if (mode === "artifact-only") {
|
|
2803
|
+
if (src?.mode === "local-file") {
|
|
2804
|
+
await this._loadFromLocalFile(ctx, src.path, src.fetchTimeoutMs);
|
|
2805
|
+
} else if (src?.mode === "artifact-api") {
|
|
2806
|
+
await this._loadFromArtifactApi(ctx, src);
|
|
2807
|
+
} else {
|
|
2808
|
+
throw new Error("[MetadataPlugin] bootstrap=artifact-only requires options.artifactSource to be set");
|
|
2809
|
+
}
|
|
2810
|
+
} else if (mode === "lazy") {
|
|
2811
|
+
if (src?.mode === "local-file") {
|
|
2812
|
+
await this._loadFromLocalFile(ctx, src.path, src.fetchTimeoutMs);
|
|
2813
|
+
} else if (src?.mode === "artifact-api") {
|
|
2814
|
+
await this._loadFromArtifactApi(ctx, src);
|
|
2815
|
+
} else {
|
|
2816
|
+
ctx.logger.info("[MetadataPlugin] lazy bootstrap \u2014 skipping filesystem priming; metadata loads on demand");
|
|
2817
|
+
}
|
|
2818
|
+
} else {
|
|
2819
|
+
if (src?.mode === "local-file") {
|
|
2820
|
+
await this._loadFromLocalFile(ctx, src.path, src.fetchTimeoutMs);
|
|
2821
|
+
} else if (src?.mode === "artifact-api") {
|
|
2822
|
+
await this._loadFromArtifactApi(ctx, src);
|
|
2823
|
+
} else {
|
|
2824
|
+
await this._loadFromFileSystem(ctx);
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
try {
|
|
2828
|
+
const realtimeService = ctx.getService("realtime");
|
|
2829
|
+
if (realtimeService && typeof realtimeService === "object" && "publish" in realtimeService) {
|
|
2830
|
+
ctx.logger.info("[MetadataPlugin] Bridging realtime service to MetadataManager for event publishing");
|
|
2831
|
+
this.manager.setRealtimeService(realtimeService);
|
|
2832
|
+
}
|
|
2833
|
+
} catch (e) {
|
|
2834
|
+
ctx.logger.debug("[MetadataPlugin] No realtime service found \u2014 metadata events will not be published", {
|
|
2835
|
+
error: e.message
|
|
2836
|
+
});
|
|
2837
|
+
}
|
|
2838
|
+
};
|
|
2839
|
+
this.options = {
|
|
2840
|
+
watch: true,
|
|
2841
|
+
...options
|
|
2842
|
+
};
|
|
2843
|
+
const rootDir = this.options.rootDir || process.cwd();
|
|
2844
|
+
const bootstrapMode = this.options.config?.bootstrap ?? "eager";
|
|
2845
|
+
const effectiveWatch = bootstrapMode === "artifact-only" ? false : this.options.watch ?? true;
|
|
2846
|
+
this.manager = new NodeMetadataManager({
|
|
2847
|
+
rootDir,
|
|
2848
|
+
watch: effectiveWatch,
|
|
2849
|
+
formats: ["yaml", "json", "typescript", "javascript"]
|
|
2850
|
+
});
|
|
2851
|
+
this.manager.setTypeRegistry(import_kernel.DEFAULT_METADATA_TYPE_REGISTRY);
|
|
2852
|
+
}
|
|
2853
|
+
/**
|
|
2854
|
+
* Fetch JSON content from a URL with configurable timeout.
|
|
2855
|
+
*/
|
|
2856
|
+
async _fetchJson(url, fetchTimeoutMs, token) {
|
|
2857
|
+
const envTimeout = Number(process.env.OS_ARTIFACT_FETCH_TIMEOUT_MS);
|
|
2858
|
+
const timeoutMs = fetchTimeoutMs ?? (Number.isFinite(envTimeout) && envTimeout > 0 ? envTimeout : void 0) ?? 6e4;
|
|
2859
|
+
const controller = new AbortController();
|
|
2860
|
+
const timer = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : void 0;
|
|
2861
|
+
try {
|
|
2862
|
+
const headers = { Accept: "application/json, */*;q=0.5" };
|
|
2863
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
2864
|
+
const res = await fetch(url, { redirect: "follow", signal: controller.signal, headers });
|
|
2865
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
2866
|
+
const content = await res.text();
|
|
2867
|
+
return JSON.parse(content);
|
|
2868
|
+
} catch (e) {
|
|
2869
|
+
if (e?.name === "AbortError") {
|
|
2870
|
+
throw new Error(
|
|
2871
|
+
`fetch timed out after ${timeoutMs}ms \u2014 set artifactSource.fetchTimeoutMs or OS_ARTIFACT_FETCH_TIMEOUT_MS to extend it (0 disables)`
|
|
2872
|
+
);
|
|
2873
|
+
}
|
|
2874
|
+
throw e;
|
|
2875
|
+
} finally {
|
|
2876
|
+
if (timer) clearTimeout(timer);
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
/**
|
|
2880
|
+
* Parse raw artifact JSON (envelope or bare definition) and register all
|
|
2881
|
+
* metadata items into the MetadataManager.
|
|
2882
|
+
*/
|
|
2883
|
+
async _parseAndRegisterArtifact(ctx, raw, label) {
|
|
2884
|
+
const { ProjectArtifactSchema } = await import("@objectstack/spec/cloud");
|
|
2885
|
+
const { ObjectStackDefinitionSchema } = await import("@objectstack/spec");
|
|
2886
|
+
let metadata;
|
|
2887
|
+
const obj = raw;
|
|
2888
|
+
if (obj?.schemaVersion && obj?.commitId && obj?.metadata !== void 0) {
|
|
2889
|
+
const artifact = ProjectArtifactSchema.parse(obj);
|
|
2890
|
+
metadata = artifact.metadata;
|
|
2891
|
+
} else if (obj?.success && obj?.data?.metadata) {
|
|
2892
|
+
const artifact = ProjectArtifactSchema.parse(obj.data);
|
|
2893
|
+
metadata = artifact.metadata;
|
|
2894
|
+
} else {
|
|
2895
|
+
const def = ObjectStackDefinitionSchema.parse(obj);
|
|
2896
|
+
const canonical = JSON.stringify(def, Object.keys(def).sort());
|
|
2897
|
+
const checksum = (0, import_node_crypto2.createHash)("sha256").update(canonical).digest("hex");
|
|
2898
|
+
const projectId = this.options.projectId ?? "proj_local";
|
|
2899
|
+
ProjectArtifactSchema.parse({
|
|
2900
|
+
schemaVersion: "0.1",
|
|
2901
|
+
projectId,
|
|
2902
|
+
commitId: "local-dev",
|
|
2903
|
+
checksum,
|
|
2904
|
+
metadata: def
|
|
2905
|
+
});
|
|
2906
|
+
metadata = def;
|
|
2907
|
+
}
|
|
2908
|
+
const memLoader = new MemoryLoader();
|
|
2909
|
+
const manifestPackageId = metadata?.manifest?.id ?? metadata?.id ?? void 0;
|
|
2910
|
+
let totalRegistered = 0;
|
|
2911
|
+
for (const [field, metaType] of Object.entries(ARTIFACT_FIELD_TO_TYPE)) {
|
|
2912
|
+
const items = metadata[field];
|
|
2913
|
+
if (!Array.isArray(items) || items.length === 0) continue;
|
|
2914
|
+
for (const item of items) {
|
|
2915
|
+
const name = item?.name;
|
|
2916
|
+
if (!name) continue;
|
|
2917
|
+
if (manifestPackageId && item._packageId === void 0) {
|
|
2918
|
+
item._packageId = manifestPackageId;
|
|
2919
|
+
}
|
|
2920
|
+
await memLoader.save(metaType, name, item);
|
|
2921
|
+
await this.manager.register(metaType, name, item);
|
|
2922
|
+
totalRegistered++;
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
this.manager.registerLoader(memLoader);
|
|
2926
|
+
ctx.logger.info("[MetadataPlugin] Artifact metadata loaded", { source: label, totalRegistered });
|
|
2927
|
+
return totalRegistered;
|
|
2928
|
+
}
|
|
2929
|
+
async _loadFromLocalFile(ctx, filePath, fetchTimeoutMs) {
|
|
2930
|
+
const isUrl = /^https?:\/\//i.test(filePath);
|
|
2931
|
+
ctx.logger.info(
|
|
2932
|
+
`[MetadataPlugin] Loading metadata from ${isUrl ? "remote URL" : "local artifact file"}`,
|
|
2933
|
+
{ path: filePath }
|
|
2934
|
+
);
|
|
2935
|
+
let raw;
|
|
2936
|
+
try {
|
|
2937
|
+
if (isUrl) {
|
|
2938
|
+
raw = await this._fetchJson(filePath, fetchTimeoutMs);
|
|
2939
|
+
} else {
|
|
2940
|
+
const content = await (0, import_promises.readFile)(filePath, "utf8");
|
|
2941
|
+
raw = JSON.parse(content);
|
|
2942
|
+
}
|
|
2943
|
+
} catch (e) {
|
|
2944
|
+
throw new Error(`[MetadataPlugin] Cannot read artifact ${isUrl ? "URL" : "file"} at "${filePath}": ${e.message}`);
|
|
2945
|
+
}
|
|
2946
|
+
await this._parseAndRegisterArtifact(ctx, raw, filePath);
|
|
2947
|
+
}
|
|
2948
|
+
/**
|
|
2949
|
+
* P2: Load metadata from the cloud artifact API endpoint.
|
|
2950
|
+
*/
|
|
2951
|
+
async _loadFromArtifactApi(ctx, src) {
|
|
2952
|
+
const projectId = this.options.projectId;
|
|
2953
|
+
if (!projectId) {
|
|
2954
|
+
throw new Error("[MetadataPlugin] artifact-api source requires options.projectId to be set");
|
|
2955
|
+
}
|
|
2956
|
+
let artifactUrl = src.url.replace(/\/+$/, "");
|
|
2957
|
+
if (!/\/api\/v\d+\/cloud\/projects\//i.test(artifactUrl)) {
|
|
2958
|
+
artifactUrl = `${artifactUrl}/api/v1/cloud/projects/${projectId}/artifact`;
|
|
2959
|
+
}
|
|
2960
|
+
if (src.commitId) {
|
|
2961
|
+
artifactUrl += `${artifactUrl.includes("?") ? "&" : "?"}commit=${encodeURIComponent(src.commitId)}`;
|
|
2962
|
+
}
|
|
2963
|
+
ctx.logger.info("[MetadataPlugin] Loading metadata from artifact API", { url: artifactUrl });
|
|
2964
|
+
let raw;
|
|
2965
|
+
try {
|
|
2966
|
+
raw = await this._fetchJson(artifactUrl, src.fetchTimeoutMs, src.token);
|
|
2967
|
+
} catch (e) {
|
|
2968
|
+
throw new Error(`[MetadataPlugin] Cannot load artifact from API "${artifactUrl}": ${e.message}`);
|
|
2969
|
+
}
|
|
2970
|
+
await this._parseAndRegisterArtifact(ctx, raw, artifactUrl);
|
|
2971
|
+
}
|
|
2972
|
+
async _loadFromFileSystem(ctx) {
|
|
2973
|
+
ctx.logger.info("Loading metadata from file system...");
|
|
2974
|
+
const sortedTypes = [...import_kernel.DEFAULT_METADATA_TYPE_REGISTRY].sort((a, b) => a.loadOrder - b.loadOrder);
|
|
2975
|
+
let totalLoaded = 0;
|
|
2976
|
+
for (const entry of sortedTypes) {
|
|
2977
|
+
try {
|
|
2978
|
+
const items = await this.manager.loadMany(entry.type, {
|
|
2979
|
+
recursive: true,
|
|
2980
|
+
patterns: entry.filePatterns
|
|
2981
|
+
});
|
|
2982
|
+
if (items.length > 0) {
|
|
2983
|
+
for (const item of items) {
|
|
2984
|
+
const meta = item;
|
|
2985
|
+
if (meta?.name) {
|
|
2986
|
+
await this.manager.register(entry.type, meta.name, item);
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
ctx.logger.info(`Loaded ${items.length} ${entry.type} from file system`);
|
|
2990
|
+
totalLoaded += items.length;
|
|
2991
|
+
}
|
|
2992
|
+
} catch (e) {
|
|
2993
|
+
ctx.logger.debug(`No ${entry.type} metadata found`, { error: e.message });
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
ctx.logger.info("Metadata loading complete", {
|
|
2997
|
+
totalItems: totalLoaded,
|
|
2998
|
+
registeredTypes: sortedTypes.length
|
|
2999
|
+
});
|
|
3000
|
+
}
|
|
3001
|
+
};
|
|
3002
|
+
|
|
2750
3003
|
// src/loaders/remote-loader.ts
|
|
2751
3004
|
var RemoteLoader = class {
|
|
2752
3005
|
constructor(baseUrl, authToken) {
|
|
@@ -2844,6 +3097,9 @@ var RemoteLoader = class {
|
|
|
2844
3097
|
}
|
|
2845
3098
|
};
|
|
2846
3099
|
|
|
3100
|
+
// src/index.ts
|
|
3101
|
+
var import_metadata3 = require("@objectstack/platform-objects/metadata");
|
|
3102
|
+
|
|
2847
3103
|
// src/routes/history-routes.ts
|
|
2848
3104
|
function registerMetadataHistoryRoutes(app, metadataService) {
|
|
2849
3105
|
app.get("/api/v1/metadata/:type/:name/history", async (c) => {
|
|
@@ -2999,7 +3255,8 @@ var HistoryCleanupManager = class {
|
|
|
2999
3255
|
async runCleanup() {
|
|
3000
3256
|
const driver = this.dbLoader.driver;
|
|
3001
3257
|
const historyTableName = this.dbLoader.historyTableName;
|
|
3002
|
-
const
|
|
3258
|
+
const organizationId = this.dbLoader.organizationId;
|
|
3259
|
+
const projectId = this.dbLoader.projectId;
|
|
3003
3260
|
let deleted = 0;
|
|
3004
3261
|
let errors = 0;
|
|
3005
3262
|
try {
|
|
@@ -3010,8 +3267,11 @@ var HistoryCleanupManager = class {
|
|
|
3010
3267
|
const filter = {
|
|
3011
3268
|
recorded_at: { $lt: cutoffISO }
|
|
3012
3269
|
};
|
|
3013
|
-
if (
|
|
3014
|
-
filter.
|
|
3270
|
+
if (organizationId) {
|
|
3271
|
+
filter.organization_id = organizationId;
|
|
3272
|
+
}
|
|
3273
|
+
if (projectId !== void 0) {
|
|
3274
|
+
filter.project_id = projectId;
|
|
3015
3275
|
}
|
|
3016
3276
|
try {
|
|
3017
3277
|
const result = await this.bulkDeleteByFilter(driver, historyTableName, filter);
|
|
@@ -3023,9 +3283,12 @@ var HistoryCleanupManager = class {
|
|
|
3023
3283
|
}
|
|
3024
3284
|
if (this.policy.maxVersions) {
|
|
3025
3285
|
try {
|
|
3286
|
+
const baseWhere = {};
|
|
3287
|
+
if (organizationId) baseWhere.organization_id = organizationId;
|
|
3288
|
+
if (projectId !== void 0) baseWhere.project_id = projectId;
|
|
3026
3289
|
const metadataIds = await driver.find(historyTableName, {
|
|
3027
3290
|
object: historyTableName,
|
|
3028
|
-
where:
|
|
3291
|
+
where: baseWhere,
|
|
3029
3292
|
fields: ["metadata_id"]
|
|
3030
3293
|
});
|
|
3031
3294
|
const uniqueIds = /* @__PURE__ */ new Set();
|
|
@@ -3035,10 +3298,7 @@ var HistoryCleanupManager = class {
|
|
|
3035
3298
|
}
|
|
3036
3299
|
}
|
|
3037
3300
|
for (const metadataId of uniqueIds) {
|
|
3038
|
-
const filter = { metadata_id: metadataId };
|
|
3039
|
-
if (tenantId) {
|
|
3040
|
-
filter.tenant_id = tenantId;
|
|
3041
|
-
}
|
|
3301
|
+
const filter = { metadata_id: metadataId, ...baseWhere };
|
|
3042
3302
|
try {
|
|
3043
3303
|
const historyRecords = await driver.find(historyTableName, {
|
|
3044
3304
|
object: historyTableName,
|
|
@@ -3112,20 +3372,22 @@ var HistoryCleanupManager = class {
|
|
|
3112
3372
|
async getCleanupStats() {
|
|
3113
3373
|
const driver = this.dbLoader.driver;
|
|
3114
3374
|
const historyTableName = this.dbLoader.historyTableName;
|
|
3115
|
-
const
|
|
3375
|
+
const organizationId = this.dbLoader.organizationId;
|
|
3376
|
+
const projectId = this.dbLoader.projectId;
|
|
3116
3377
|
let recordsByAge = 0;
|
|
3117
3378
|
let recordsByCount = 0;
|
|
3118
3379
|
try {
|
|
3380
|
+
const baseWhere = {};
|
|
3381
|
+
if (organizationId) baseWhere.organization_id = organizationId;
|
|
3382
|
+
if (projectId !== void 0) baseWhere.project_id = projectId;
|
|
3119
3383
|
if (this.policy.maxAgeDays) {
|
|
3120
3384
|
const cutoffDate = /* @__PURE__ */ new Date();
|
|
3121
3385
|
cutoffDate.setDate(cutoffDate.getDate() - this.policy.maxAgeDays);
|
|
3122
3386
|
const cutoffISO = cutoffDate.toISOString();
|
|
3123
3387
|
const filter = {
|
|
3124
|
-
recorded_at: { $lt: cutoffISO }
|
|
3388
|
+
recorded_at: { $lt: cutoffISO },
|
|
3389
|
+
...baseWhere
|
|
3125
3390
|
};
|
|
3126
|
-
if (tenantId) {
|
|
3127
|
-
filter.tenant_id = tenantId;
|
|
3128
|
-
}
|
|
3129
3391
|
recordsByAge = await driver.count(historyTableName, {
|
|
3130
3392
|
object: historyTableName,
|
|
3131
3393
|
where: filter
|
|
@@ -3134,7 +3396,7 @@ var HistoryCleanupManager = class {
|
|
|
3134
3396
|
if (this.policy.maxVersions) {
|
|
3135
3397
|
const metadataIds = await driver.find(historyTableName, {
|
|
3136
3398
|
object: historyTableName,
|
|
3137
|
-
where:
|
|
3399
|
+
where: baseWhere,
|
|
3138
3400
|
fields: ["metadata_id"]
|
|
3139
3401
|
});
|
|
3140
3402
|
const uniqueIds = /* @__PURE__ */ new Set();
|
|
@@ -3144,10 +3406,7 @@ var HistoryCleanupManager = class {
|
|
|
3144
3406
|
}
|
|
3145
3407
|
}
|
|
3146
3408
|
for (const metadataId of uniqueIds) {
|
|
3147
|
-
const filter = { metadata_id: metadataId };
|
|
3148
|
-
if (tenantId) {
|
|
3149
|
-
filter.tenant_id = tenantId;
|
|
3150
|
-
}
|
|
3409
|
+
const filter = { metadata_id: metadataId, ...baseWhere };
|
|
3151
3410
|
const count = await driver.count(historyTableName, {
|
|
3152
3411
|
object: historyTableName,
|
|
3153
3412
|
where: filter
|