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