@objectstack/metadata 3.0.6 → 3.0.7
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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +10 -0
- package/ROADMAP.md +14 -13
- package/dist/index.d.mts +398 -2
- package/dist/index.d.ts +398 -2
- package/dist/index.js +411 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +409 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
- package/src/index.ts +4 -0
- package/src/loaders/database-loader.test.ts +559 -0
- package/src/loaders/database-loader.ts +352 -0
- package/src/metadata-manager.ts +25 -0
- package/src/node.ts +1 -0
- package/src/objects/sys-metadata.object.ts +187 -0
- package/vitest.config.ts +1 -0
package/dist/index.js
CHANGED
|
@@ -30,12 +30,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
DatabaseLoader: () => DatabaseLoader,
|
|
33
34
|
JSONSerializer: () => JSONSerializer,
|
|
34
35
|
MemoryLoader: () => MemoryLoader,
|
|
35
36
|
MetadataManager: () => MetadataManager,
|
|
36
37
|
MetadataPlugin: () => MetadataPlugin,
|
|
37
38
|
Migration: () => migration_exports,
|
|
38
39
|
RemoteLoader: () => RemoteLoader,
|
|
40
|
+
SysMetadataObject: () => SysMetadataObject,
|
|
39
41
|
TypeScriptSerializer: () => TypeScriptSerializer,
|
|
40
42
|
YAMLSerializer: () => YAMLSerializer
|
|
41
43
|
});
|
|
@@ -211,6 +213,395 @@ export default metadata;
|
|
|
211
213
|
}
|
|
212
214
|
};
|
|
213
215
|
|
|
216
|
+
// src/objects/sys-metadata.object.ts
|
|
217
|
+
var import_data = require("@objectstack/spec/data");
|
|
218
|
+
var SysMetadataObject = import_data.ObjectSchema.create({
|
|
219
|
+
name: "sys_metadata",
|
|
220
|
+
label: "System Metadata",
|
|
221
|
+
pluralLabel: "System Metadata",
|
|
222
|
+
icon: "settings",
|
|
223
|
+
isSystem: true,
|
|
224
|
+
description: "Stores platform and user-scope metadata records (objects, views, flows, etc.)",
|
|
225
|
+
fields: {
|
|
226
|
+
/** Primary Key (UUID) */
|
|
227
|
+
id: import_data.Field.text({
|
|
228
|
+
label: "ID",
|
|
229
|
+
required: true,
|
|
230
|
+
readonly: true
|
|
231
|
+
}),
|
|
232
|
+
/** Machine name — unique identifier used in code references */
|
|
233
|
+
name: import_data.Field.text({
|
|
234
|
+
label: "Name",
|
|
235
|
+
required: true,
|
|
236
|
+
searchable: true,
|
|
237
|
+
maxLength: 255
|
|
238
|
+
}),
|
|
239
|
+
/** Metadata type (e.g. "object", "view", "flow") */
|
|
240
|
+
type: import_data.Field.text({
|
|
241
|
+
label: "Metadata Type",
|
|
242
|
+
required: true,
|
|
243
|
+
searchable: true,
|
|
244
|
+
maxLength: 100
|
|
245
|
+
}),
|
|
246
|
+
/** Namespace / module grouping (e.g. "crm", "core") */
|
|
247
|
+
namespace: import_data.Field.text({
|
|
248
|
+
label: "Namespace",
|
|
249
|
+
required: false,
|
|
250
|
+
defaultValue: "default",
|
|
251
|
+
maxLength: 100
|
|
252
|
+
}),
|
|
253
|
+
/** Package that owns/delivered this metadata */
|
|
254
|
+
package_id: import_data.Field.text({
|
|
255
|
+
label: "Package ID",
|
|
256
|
+
required: false,
|
|
257
|
+
maxLength: 255
|
|
258
|
+
}),
|
|
259
|
+
/** Who manages this record: package, platform, or user */
|
|
260
|
+
managed_by: import_data.Field.select(["package", "platform", "user"], {
|
|
261
|
+
label: "Managed By",
|
|
262
|
+
required: false
|
|
263
|
+
}),
|
|
264
|
+
/** Scope: system (code), platform (admin DB), user (personal DB) */
|
|
265
|
+
scope: import_data.Field.select(["system", "platform", "user"], {
|
|
266
|
+
label: "Scope",
|
|
267
|
+
required: true,
|
|
268
|
+
defaultValue: "platform"
|
|
269
|
+
}),
|
|
270
|
+
/** JSON payload — the actual metadata configuration */
|
|
271
|
+
metadata: import_data.Field.textarea({
|
|
272
|
+
label: "Metadata",
|
|
273
|
+
required: true,
|
|
274
|
+
description: "JSON-serialized metadata payload"
|
|
275
|
+
}),
|
|
276
|
+
/** Parent metadata name for extension/override */
|
|
277
|
+
extends: import_data.Field.text({
|
|
278
|
+
label: "Extends",
|
|
279
|
+
required: false,
|
|
280
|
+
maxLength: 255
|
|
281
|
+
}),
|
|
282
|
+
/** Merge strategy when extending parent metadata */
|
|
283
|
+
strategy: import_data.Field.select(["merge", "replace"], {
|
|
284
|
+
label: "Strategy",
|
|
285
|
+
required: false,
|
|
286
|
+
defaultValue: "merge"
|
|
287
|
+
}),
|
|
288
|
+
/** Owner user ID (for user-scope items) */
|
|
289
|
+
owner: import_data.Field.text({
|
|
290
|
+
label: "Owner",
|
|
291
|
+
required: false,
|
|
292
|
+
maxLength: 255
|
|
293
|
+
}),
|
|
294
|
+
/** Lifecycle state */
|
|
295
|
+
state: import_data.Field.select(["draft", "active", "archived", "deprecated"], {
|
|
296
|
+
label: "State",
|
|
297
|
+
required: false,
|
|
298
|
+
defaultValue: "active"
|
|
299
|
+
}),
|
|
300
|
+
/** Tenant ID for multi-tenant isolation */
|
|
301
|
+
tenant_id: import_data.Field.text({
|
|
302
|
+
label: "Tenant ID",
|
|
303
|
+
required: false,
|
|
304
|
+
maxLength: 255
|
|
305
|
+
}),
|
|
306
|
+
/** Version number for optimistic concurrency */
|
|
307
|
+
version: import_data.Field.number({
|
|
308
|
+
label: "Version",
|
|
309
|
+
required: false,
|
|
310
|
+
defaultValue: 1
|
|
311
|
+
}),
|
|
312
|
+
/** Content checksum for change detection */
|
|
313
|
+
checksum: import_data.Field.text({
|
|
314
|
+
label: "Checksum",
|
|
315
|
+
required: false,
|
|
316
|
+
maxLength: 64
|
|
317
|
+
}),
|
|
318
|
+
/** Origin of this metadata record */
|
|
319
|
+
source: import_data.Field.select(["filesystem", "database", "api", "migration"], {
|
|
320
|
+
label: "Source",
|
|
321
|
+
required: false
|
|
322
|
+
}),
|
|
323
|
+
/** Classification tags (JSON array) */
|
|
324
|
+
tags: import_data.Field.textarea({
|
|
325
|
+
label: "Tags",
|
|
326
|
+
required: false,
|
|
327
|
+
description: "JSON-serialized array of classification tags"
|
|
328
|
+
}),
|
|
329
|
+
/** Audit fields */
|
|
330
|
+
created_by: import_data.Field.text({
|
|
331
|
+
label: "Created By",
|
|
332
|
+
required: false,
|
|
333
|
+
readonly: true,
|
|
334
|
+
maxLength: 255
|
|
335
|
+
}),
|
|
336
|
+
created_at: import_data.Field.datetime({
|
|
337
|
+
label: "Created At",
|
|
338
|
+
required: false,
|
|
339
|
+
readonly: true
|
|
340
|
+
}),
|
|
341
|
+
updated_by: import_data.Field.text({
|
|
342
|
+
label: "Updated By",
|
|
343
|
+
required: false,
|
|
344
|
+
maxLength: 255
|
|
345
|
+
}),
|
|
346
|
+
updated_at: import_data.Field.datetime({
|
|
347
|
+
label: "Updated At",
|
|
348
|
+
required: false
|
|
349
|
+
})
|
|
350
|
+
},
|
|
351
|
+
indexes: [
|
|
352
|
+
{ fields: ["type", "name"], unique: true },
|
|
353
|
+
{ fields: ["type", "scope"] },
|
|
354
|
+
{ fields: ["tenant_id"] },
|
|
355
|
+
{ fields: ["state"] },
|
|
356
|
+
{ fields: ["namespace"] }
|
|
357
|
+
],
|
|
358
|
+
enable: {
|
|
359
|
+
trackHistory: true,
|
|
360
|
+
searchable: false,
|
|
361
|
+
apiEnabled: true,
|
|
362
|
+
apiMethods: ["get", "list", "create", "update", "delete"],
|
|
363
|
+
trash: false
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// src/loaders/database-loader.ts
|
|
368
|
+
var DatabaseLoader = class {
|
|
369
|
+
constructor(options) {
|
|
370
|
+
this.contract = {
|
|
371
|
+
name: "database",
|
|
372
|
+
protocol: "datasource:",
|
|
373
|
+
capabilities: {
|
|
374
|
+
read: true,
|
|
375
|
+
write: true,
|
|
376
|
+
watch: false,
|
|
377
|
+
list: true
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
this.schemaReady = false;
|
|
381
|
+
this.driver = options.driver;
|
|
382
|
+
this.tableName = options.tableName ?? "sys_metadata";
|
|
383
|
+
this.tenantId = options.tenantId;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Ensure the metadata table exists.
|
|
387
|
+
* Uses IDataDriver.syncSchema with the SysMetadataObject definition
|
|
388
|
+
* to idempotently create/update the table.
|
|
389
|
+
*/
|
|
390
|
+
async ensureSchema() {
|
|
391
|
+
if (this.schemaReady) return;
|
|
392
|
+
try {
|
|
393
|
+
await this.driver.syncSchema(this.tableName, {
|
|
394
|
+
...SysMetadataObject,
|
|
395
|
+
name: this.tableName
|
|
396
|
+
});
|
|
397
|
+
this.schemaReady = true;
|
|
398
|
+
} catch {
|
|
399
|
+
this.schemaReady = true;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Build base filter conditions for queries.
|
|
404
|
+
* Always includes tenantId when configured.
|
|
405
|
+
*/
|
|
406
|
+
baseFilter(type, name) {
|
|
407
|
+
const filter = { type };
|
|
408
|
+
if (name !== void 0) {
|
|
409
|
+
filter.name = name;
|
|
410
|
+
}
|
|
411
|
+
if (this.tenantId) {
|
|
412
|
+
filter.tenant_id = this.tenantId;
|
|
413
|
+
}
|
|
414
|
+
return filter;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Convert a database row to a metadata payload.
|
|
418
|
+
* Parses the JSON `metadata` column back into an object.
|
|
419
|
+
*/
|
|
420
|
+
rowToData(row) {
|
|
421
|
+
if (!row || !row.metadata) return null;
|
|
422
|
+
const payload = typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata;
|
|
423
|
+
return payload;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Convert a database row to a MetadataRecord-like object.
|
|
427
|
+
*/
|
|
428
|
+
rowToRecord(row) {
|
|
429
|
+
return {
|
|
430
|
+
id: row.id,
|
|
431
|
+
name: row.name,
|
|
432
|
+
type: row.type,
|
|
433
|
+
namespace: row.namespace ?? "default",
|
|
434
|
+
packageId: row.package_id,
|
|
435
|
+
managedBy: row.managed_by,
|
|
436
|
+
scope: row.scope ?? "platform",
|
|
437
|
+
metadata: this.rowToData(row) ?? {},
|
|
438
|
+
extends: row.extends,
|
|
439
|
+
strategy: row.strategy ?? "merge",
|
|
440
|
+
owner: row.owner,
|
|
441
|
+
state: row.state ?? "active",
|
|
442
|
+
tenantId: row.tenant_id,
|
|
443
|
+
version: row.version ?? 1,
|
|
444
|
+
checksum: row.checksum,
|
|
445
|
+
source: row.source,
|
|
446
|
+
tags: row.tags ? typeof row.tags === "string" ? JSON.parse(row.tags) : row.tags : void 0,
|
|
447
|
+
createdBy: row.created_by,
|
|
448
|
+
createdAt: row.created_at,
|
|
449
|
+
updatedBy: row.updated_by,
|
|
450
|
+
updatedAt: row.updated_at
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
// ==========================================
|
|
454
|
+
// MetadataLoader Interface Implementation
|
|
455
|
+
// ==========================================
|
|
456
|
+
async load(type, name, _options) {
|
|
457
|
+
const startTime = Date.now();
|
|
458
|
+
await this.ensureSchema();
|
|
459
|
+
try {
|
|
460
|
+
const row = await this.driver.findOne(this.tableName, {
|
|
461
|
+
object: this.tableName,
|
|
462
|
+
where: this.baseFilter(type, name)
|
|
463
|
+
});
|
|
464
|
+
if (!row) {
|
|
465
|
+
return {
|
|
466
|
+
data: null,
|
|
467
|
+
loadTime: Date.now() - startTime
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
const data = this.rowToData(row);
|
|
471
|
+
const record = this.rowToRecord(row);
|
|
472
|
+
return {
|
|
473
|
+
data,
|
|
474
|
+
source: "database",
|
|
475
|
+
format: "json",
|
|
476
|
+
etag: record.checksum,
|
|
477
|
+
loadTime: Date.now() - startTime
|
|
478
|
+
};
|
|
479
|
+
} catch {
|
|
480
|
+
return {
|
|
481
|
+
data: null,
|
|
482
|
+
loadTime: Date.now() - startTime
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
async loadMany(type, _options) {
|
|
487
|
+
await this.ensureSchema();
|
|
488
|
+
try {
|
|
489
|
+
const rows = await this.driver.find(this.tableName, {
|
|
490
|
+
object: this.tableName,
|
|
491
|
+
where: this.baseFilter(type)
|
|
492
|
+
});
|
|
493
|
+
return rows.map((row) => this.rowToData(row)).filter((data) => data !== null);
|
|
494
|
+
} catch {
|
|
495
|
+
return [];
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
async exists(type, name) {
|
|
499
|
+
await this.ensureSchema();
|
|
500
|
+
try {
|
|
501
|
+
const count = await this.driver.count(this.tableName, {
|
|
502
|
+
object: this.tableName,
|
|
503
|
+
where: this.baseFilter(type, name)
|
|
504
|
+
});
|
|
505
|
+
return count > 0;
|
|
506
|
+
} catch {
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
async stat(type, name) {
|
|
511
|
+
await this.ensureSchema();
|
|
512
|
+
try {
|
|
513
|
+
const row = await this.driver.findOne(this.tableName, {
|
|
514
|
+
object: this.tableName,
|
|
515
|
+
where: this.baseFilter(type, name)
|
|
516
|
+
});
|
|
517
|
+
if (!row) return null;
|
|
518
|
+
const record = this.rowToRecord(row);
|
|
519
|
+
const metadataStr = typeof row.metadata === "string" ? row.metadata : JSON.stringify(row.metadata);
|
|
520
|
+
return {
|
|
521
|
+
size: metadataStr.length,
|
|
522
|
+
mtime: record.updatedAt ?? record.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
523
|
+
format: "json",
|
|
524
|
+
etag: record.checksum
|
|
525
|
+
};
|
|
526
|
+
} catch {
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
async list(type) {
|
|
531
|
+
await this.ensureSchema();
|
|
532
|
+
try {
|
|
533
|
+
const rows = await this.driver.find(this.tableName, {
|
|
534
|
+
object: this.tableName,
|
|
535
|
+
where: this.baseFilter(type),
|
|
536
|
+
fields: ["name"]
|
|
537
|
+
});
|
|
538
|
+
return rows.map((row) => row.name).filter((name) => typeof name === "string");
|
|
539
|
+
} catch {
|
|
540
|
+
return [];
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
async save(type, name, data, _options) {
|
|
544
|
+
const startTime = Date.now();
|
|
545
|
+
await this.ensureSchema();
|
|
546
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
547
|
+
const metadataJson = JSON.stringify(data);
|
|
548
|
+
try {
|
|
549
|
+
const existing = await this.driver.findOne(this.tableName, {
|
|
550
|
+
object: this.tableName,
|
|
551
|
+
where: this.baseFilter(type, name)
|
|
552
|
+
});
|
|
553
|
+
if (existing) {
|
|
554
|
+
const version = (existing.version ?? 0) + 1;
|
|
555
|
+
await this.driver.update(this.tableName, existing.id, {
|
|
556
|
+
metadata: metadataJson,
|
|
557
|
+
version,
|
|
558
|
+
updated_at: now,
|
|
559
|
+
state: "active"
|
|
560
|
+
});
|
|
561
|
+
return {
|
|
562
|
+
success: true,
|
|
563
|
+
path: `datasource://${this.tableName}/${type}/${name}`,
|
|
564
|
+
size: metadataJson.length,
|
|
565
|
+
saveTime: Date.now() - startTime
|
|
566
|
+
};
|
|
567
|
+
} else {
|
|
568
|
+
const id = generateId();
|
|
569
|
+
await this.driver.create(this.tableName, {
|
|
570
|
+
id,
|
|
571
|
+
name,
|
|
572
|
+
type,
|
|
573
|
+
namespace: "default",
|
|
574
|
+
scope: data?.scope ?? "platform",
|
|
575
|
+
metadata: metadataJson,
|
|
576
|
+
strategy: "merge",
|
|
577
|
+
state: "active",
|
|
578
|
+
version: 1,
|
|
579
|
+
source: "database",
|
|
580
|
+
...this.tenantId ? { tenant_id: this.tenantId } : {},
|
|
581
|
+
created_at: now,
|
|
582
|
+
updated_at: now
|
|
583
|
+
});
|
|
584
|
+
return {
|
|
585
|
+
success: true,
|
|
586
|
+
path: `datasource://${this.tableName}/${type}/${name}`,
|
|
587
|
+
size: metadataJson.length,
|
|
588
|
+
saveTime: Date.now() - startTime
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
} catch (error) {
|
|
592
|
+
throw new Error(
|
|
593
|
+
`DatabaseLoader save failed for ${type}/${name}: ${error instanceof Error ? error.message : String(error)}`
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
function generateId() {
|
|
599
|
+
if (typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.randomUUID === "function") {
|
|
600
|
+
return globalThis.crypto.randomUUID();
|
|
601
|
+
}
|
|
602
|
+
return `meta_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
|
|
603
|
+
}
|
|
604
|
+
|
|
214
605
|
// src/metadata-manager.ts
|
|
215
606
|
var MetadataManager = class {
|
|
216
607
|
constructor(config) {
|
|
@@ -243,6 +634,9 @@ var MetadataManager = class {
|
|
|
243
634
|
if (config.loaders && config.loaders.length > 0) {
|
|
244
635
|
config.loaders.forEach((loader) => this.registerLoader(loader));
|
|
245
636
|
}
|
|
637
|
+
if (config.datasource && config.driver) {
|
|
638
|
+
this.setDatabaseDriver(config.driver);
|
|
639
|
+
}
|
|
246
640
|
}
|
|
247
641
|
/**
|
|
248
642
|
* Set the type registry for metadata type discovery.
|
|
@@ -250,6 +644,21 @@ var MetadataManager = class {
|
|
|
250
644
|
setTypeRegistry(entries) {
|
|
251
645
|
this.typeRegistry = entries;
|
|
252
646
|
}
|
|
647
|
+
/**
|
|
648
|
+
* Configure and register a DatabaseLoader for database-backed metadata persistence.
|
|
649
|
+
* Can be called at any time to enable database storage (e.g. after kernel resolves the driver).
|
|
650
|
+
*
|
|
651
|
+
* @param driver - An IDataDriver instance for database operations
|
|
652
|
+
*/
|
|
653
|
+
setDatabaseDriver(driver) {
|
|
654
|
+
const tableName = this.config.tableName ?? "sys_metadata";
|
|
655
|
+
const dbLoader = new DatabaseLoader({
|
|
656
|
+
driver,
|
|
657
|
+
tableName
|
|
658
|
+
});
|
|
659
|
+
this.registerLoader(dbLoader);
|
|
660
|
+
this.logger.info("DatabaseLoader configured", { datasource: this.config.datasource, tableName });
|
|
661
|
+
}
|
|
253
662
|
/**
|
|
254
663
|
* Register a new metadata loader (data source)
|
|
255
664
|
*/
|
|
@@ -1615,12 +2024,14 @@ var MigrationExecutor = class {
|
|
|
1615
2024
|
};
|
|
1616
2025
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1617
2026
|
0 && (module.exports = {
|
|
2027
|
+
DatabaseLoader,
|
|
1618
2028
|
JSONSerializer,
|
|
1619
2029
|
MemoryLoader,
|
|
1620
2030
|
MetadataManager,
|
|
1621
2031
|
MetadataPlugin,
|
|
1622
2032
|
Migration,
|
|
1623
2033
|
RemoteLoader,
|
|
2034
|
+
SysMetadataObject,
|
|
1624
2035
|
TypeScriptSerializer,
|
|
1625
2036
|
YAMLSerializer
|
|
1626
2037
|
});
|