@objectstack/metadata 3.0.6 → 3.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -174,6 +174,395 @@ export default metadata;
174
174
  }
175
175
  };
176
176
 
177
+ // src/objects/sys-metadata.object.ts
178
+ import { ObjectSchema, Field } from "@objectstack/spec/data";
179
+ var SysMetadataObject = ObjectSchema.create({
180
+ name: "sys_metadata",
181
+ label: "System Metadata",
182
+ pluralLabel: "System Metadata",
183
+ icon: "settings",
184
+ isSystem: true,
185
+ description: "Stores platform and user-scope metadata records (objects, views, flows, etc.)",
186
+ fields: {
187
+ /** Primary Key (UUID) */
188
+ id: Field.text({
189
+ label: "ID",
190
+ required: true,
191
+ readonly: true
192
+ }),
193
+ /** Machine name — unique identifier used in code references */
194
+ name: Field.text({
195
+ label: "Name",
196
+ required: true,
197
+ searchable: true,
198
+ maxLength: 255
199
+ }),
200
+ /** Metadata type (e.g. "object", "view", "flow") */
201
+ type: Field.text({
202
+ label: "Metadata Type",
203
+ required: true,
204
+ searchable: true,
205
+ maxLength: 100
206
+ }),
207
+ /** Namespace / module grouping (e.g. "crm", "core") */
208
+ namespace: Field.text({
209
+ label: "Namespace",
210
+ required: false,
211
+ defaultValue: "default",
212
+ maxLength: 100
213
+ }),
214
+ /** Package that owns/delivered this metadata */
215
+ package_id: Field.text({
216
+ label: "Package ID",
217
+ required: false,
218
+ maxLength: 255
219
+ }),
220
+ /** Who manages this record: package, platform, or user */
221
+ managed_by: Field.select(["package", "platform", "user"], {
222
+ label: "Managed By",
223
+ required: false
224
+ }),
225
+ /** Scope: system (code), platform (admin DB), user (personal DB) */
226
+ scope: Field.select(["system", "platform", "user"], {
227
+ label: "Scope",
228
+ required: true,
229
+ defaultValue: "platform"
230
+ }),
231
+ /** JSON payload — the actual metadata configuration */
232
+ metadata: Field.textarea({
233
+ label: "Metadata",
234
+ required: true,
235
+ description: "JSON-serialized metadata payload"
236
+ }),
237
+ /** Parent metadata name for extension/override */
238
+ extends: Field.text({
239
+ label: "Extends",
240
+ required: false,
241
+ maxLength: 255
242
+ }),
243
+ /** Merge strategy when extending parent metadata */
244
+ strategy: Field.select(["merge", "replace"], {
245
+ label: "Strategy",
246
+ required: false,
247
+ defaultValue: "merge"
248
+ }),
249
+ /** Owner user ID (for user-scope items) */
250
+ owner: Field.text({
251
+ label: "Owner",
252
+ required: false,
253
+ maxLength: 255
254
+ }),
255
+ /** Lifecycle state */
256
+ state: Field.select(["draft", "active", "archived", "deprecated"], {
257
+ label: "State",
258
+ required: false,
259
+ defaultValue: "active"
260
+ }),
261
+ /** Tenant ID for multi-tenant isolation */
262
+ tenant_id: Field.text({
263
+ label: "Tenant ID",
264
+ required: false,
265
+ maxLength: 255
266
+ }),
267
+ /** Version number for optimistic concurrency */
268
+ version: Field.number({
269
+ label: "Version",
270
+ required: false,
271
+ defaultValue: 1
272
+ }),
273
+ /** Content checksum for change detection */
274
+ checksum: Field.text({
275
+ label: "Checksum",
276
+ required: false,
277
+ maxLength: 64
278
+ }),
279
+ /** Origin of this metadata record */
280
+ source: Field.select(["filesystem", "database", "api", "migration"], {
281
+ label: "Source",
282
+ required: false
283
+ }),
284
+ /** Classification tags (JSON array) */
285
+ tags: Field.textarea({
286
+ label: "Tags",
287
+ required: false,
288
+ description: "JSON-serialized array of classification tags"
289
+ }),
290
+ /** Audit fields */
291
+ created_by: Field.text({
292
+ label: "Created By",
293
+ required: false,
294
+ readonly: true,
295
+ maxLength: 255
296
+ }),
297
+ created_at: Field.datetime({
298
+ label: "Created At",
299
+ required: false,
300
+ readonly: true
301
+ }),
302
+ updated_by: Field.text({
303
+ label: "Updated By",
304
+ required: false,
305
+ maxLength: 255
306
+ }),
307
+ updated_at: Field.datetime({
308
+ label: "Updated At",
309
+ required: false
310
+ })
311
+ },
312
+ indexes: [
313
+ { fields: ["type", "name"], unique: true },
314
+ { fields: ["type", "scope"] },
315
+ { fields: ["tenant_id"] },
316
+ { fields: ["state"] },
317
+ { fields: ["namespace"] }
318
+ ],
319
+ enable: {
320
+ trackHistory: true,
321
+ searchable: false,
322
+ apiEnabled: true,
323
+ apiMethods: ["get", "list", "create", "update", "delete"],
324
+ trash: false
325
+ }
326
+ });
327
+
328
+ // src/loaders/database-loader.ts
329
+ var DatabaseLoader = class {
330
+ constructor(options) {
331
+ this.contract = {
332
+ name: "database",
333
+ protocol: "datasource:",
334
+ capabilities: {
335
+ read: true,
336
+ write: true,
337
+ watch: false,
338
+ list: true
339
+ }
340
+ };
341
+ this.schemaReady = false;
342
+ this.driver = options.driver;
343
+ this.tableName = options.tableName ?? "sys_metadata";
344
+ this.tenantId = options.tenantId;
345
+ }
346
+ /**
347
+ * Ensure the metadata table exists.
348
+ * Uses IDataDriver.syncSchema with the SysMetadataObject definition
349
+ * to idempotently create/update the table.
350
+ */
351
+ async ensureSchema() {
352
+ if (this.schemaReady) return;
353
+ try {
354
+ await this.driver.syncSchema(this.tableName, {
355
+ ...SysMetadataObject,
356
+ name: this.tableName
357
+ });
358
+ this.schemaReady = true;
359
+ } catch {
360
+ this.schemaReady = true;
361
+ }
362
+ }
363
+ /**
364
+ * Build base filter conditions for queries.
365
+ * Always includes tenantId when configured.
366
+ */
367
+ baseFilter(type, name) {
368
+ const filter = { type };
369
+ if (name !== void 0) {
370
+ filter.name = name;
371
+ }
372
+ if (this.tenantId) {
373
+ filter.tenant_id = this.tenantId;
374
+ }
375
+ return filter;
376
+ }
377
+ /**
378
+ * Convert a database row to a metadata payload.
379
+ * Parses the JSON `metadata` column back into an object.
380
+ */
381
+ rowToData(row) {
382
+ if (!row || !row.metadata) return null;
383
+ const payload = typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata;
384
+ return payload;
385
+ }
386
+ /**
387
+ * Convert a database row to a MetadataRecord-like object.
388
+ */
389
+ rowToRecord(row) {
390
+ return {
391
+ id: row.id,
392
+ name: row.name,
393
+ type: row.type,
394
+ namespace: row.namespace ?? "default",
395
+ packageId: row.package_id,
396
+ managedBy: row.managed_by,
397
+ scope: row.scope ?? "platform",
398
+ metadata: this.rowToData(row) ?? {},
399
+ extends: row.extends,
400
+ strategy: row.strategy ?? "merge",
401
+ owner: row.owner,
402
+ state: row.state ?? "active",
403
+ tenantId: row.tenant_id,
404
+ version: row.version ?? 1,
405
+ checksum: row.checksum,
406
+ source: row.source,
407
+ tags: row.tags ? typeof row.tags === "string" ? JSON.parse(row.tags) : row.tags : void 0,
408
+ createdBy: row.created_by,
409
+ createdAt: row.created_at,
410
+ updatedBy: row.updated_by,
411
+ updatedAt: row.updated_at
412
+ };
413
+ }
414
+ // ==========================================
415
+ // MetadataLoader Interface Implementation
416
+ // ==========================================
417
+ async load(type, name, _options) {
418
+ const startTime = Date.now();
419
+ await this.ensureSchema();
420
+ try {
421
+ const row = await this.driver.findOne(this.tableName, {
422
+ object: this.tableName,
423
+ where: this.baseFilter(type, name)
424
+ });
425
+ if (!row) {
426
+ return {
427
+ data: null,
428
+ loadTime: Date.now() - startTime
429
+ };
430
+ }
431
+ const data = this.rowToData(row);
432
+ const record = this.rowToRecord(row);
433
+ return {
434
+ data,
435
+ source: "database",
436
+ format: "json",
437
+ etag: record.checksum,
438
+ loadTime: Date.now() - startTime
439
+ };
440
+ } catch {
441
+ return {
442
+ data: null,
443
+ loadTime: Date.now() - startTime
444
+ };
445
+ }
446
+ }
447
+ async loadMany(type, _options) {
448
+ await this.ensureSchema();
449
+ try {
450
+ const rows = await this.driver.find(this.tableName, {
451
+ object: this.tableName,
452
+ where: this.baseFilter(type)
453
+ });
454
+ return rows.map((row) => this.rowToData(row)).filter((data) => data !== null);
455
+ } catch {
456
+ return [];
457
+ }
458
+ }
459
+ async exists(type, name) {
460
+ await this.ensureSchema();
461
+ try {
462
+ const count = await this.driver.count(this.tableName, {
463
+ object: this.tableName,
464
+ where: this.baseFilter(type, name)
465
+ });
466
+ return count > 0;
467
+ } catch {
468
+ return false;
469
+ }
470
+ }
471
+ async stat(type, name) {
472
+ await this.ensureSchema();
473
+ try {
474
+ const row = await this.driver.findOne(this.tableName, {
475
+ object: this.tableName,
476
+ where: this.baseFilter(type, name)
477
+ });
478
+ if (!row) return null;
479
+ const record = this.rowToRecord(row);
480
+ const metadataStr = typeof row.metadata === "string" ? row.metadata : JSON.stringify(row.metadata);
481
+ return {
482
+ size: metadataStr.length,
483
+ mtime: record.updatedAt ?? record.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
484
+ format: "json",
485
+ etag: record.checksum
486
+ };
487
+ } catch {
488
+ return null;
489
+ }
490
+ }
491
+ async list(type) {
492
+ await this.ensureSchema();
493
+ try {
494
+ const rows = await this.driver.find(this.tableName, {
495
+ object: this.tableName,
496
+ where: this.baseFilter(type),
497
+ fields: ["name"]
498
+ });
499
+ return rows.map((row) => row.name).filter((name) => typeof name === "string");
500
+ } catch {
501
+ return [];
502
+ }
503
+ }
504
+ async save(type, name, data, _options) {
505
+ const startTime = Date.now();
506
+ await this.ensureSchema();
507
+ const now = (/* @__PURE__ */ new Date()).toISOString();
508
+ const metadataJson = JSON.stringify(data);
509
+ try {
510
+ const existing = await this.driver.findOne(this.tableName, {
511
+ object: this.tableName,
512
+ where: this.baseFilter(type, name)
513
+ });
514
+ if (existing) {
515
+ const version = (existing.version ?? 0) + 1;
516
+ await this.driver.update(this.tableName, existing.id, {
517
+ metadata: metadataJson,
518
+ version,
519
+ updated_at: now,
520
+ state: "active"
521
+ });
522
+ return {
523
+ success: true,
524
+ path: `datasource://${this.tableName}/${type}/${name}`,
525
+ size: metadataJson.length,
526
+ saveTime: Date.now() - startTime
527
+ };
528
+ } else {
529
+ const id = generateId();
530
+ await this.driver.create(this.tableName, {
531
+ id,
532
+ name,
533
+ type,
534
+ namespace: "default",
535
+ scope: data?.scope ?? "platform",
536
+ metadata: metadataJson,
537
+ strategy: "merge",
538
+ state: "active",
539
+ version: 1,
540
+ source: "database",
541
+ ...this.tenantId ? { tenant_id: this.tenantId } : {},
542
+ created_at: now,
543
+ updated_at: now
544
+ });
545
+ return {
546
+ success: true,
547
+ path: `datasource://${this.tableName}/${type}/${name}`,
548
+ size: metadataJson.length,
549
+ saveTime: Date.now() - startTime
550
+ };
551
+ }
552
+ } catch (error) {
553
+ throw new Error(
554
+ `DatabaseLoader save failed for ${type}/${name}: ${error instanceof Error ? error.message : String(error)}`
555
+ );
556
+ }
557
+ }
558
+ };
559
+ function generateId() {
560
+ if (typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.randomUUID === "function") {
561
+ return globalThis.crypto.randomUUID();
562
+ }
563
+ return `meta_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
564
+ }
565
+
177
566
  // src/metadata-manager.ts
178
567
  var MetadataManager = class {
179
568
  constructor(config) {
@@ -206,6 +595,9 @@ var MetadataManager = class {
206
595
  if (config.loaders && config.loaders.length > 0) {
207
596
  config.loaders.forEach((loader) => this.registerLoader(loader));
208
597
  }
598
+ if (config.datasource && config.driver) {
599
+ this.setDatabaseDriver(config.driver);
600
+ }
209
601
  }
210
602
  /**
211
603
  * Set the type registry for metadata type discovery.
@@ -213,6 +605,21 @@ var MetadataManager = class {
213
605
  setTypeRegistry(entries) {
214
606
  this.typeRegistry = entries;
215
607
  }
608
+ /**
609
+ * Configure and register a DatabaseLoader for database-backed metadata persistence.
610
+ * Can be called at any time to enable database storage (e.g. after kernel resolves the driver).
611
+ *
612
+ * @param driver - An IDataDriver instance for database operations
613
+ */
614
+ setDatabaseDriver(driver) {
615
+ const tableName = this.config.tableName ?? "sys_metadata";
616
+ const dbLoader = new DatabaseLoader({
617
+ driver,
618
+ tableName
619
+ });
620
+ this.registerLoader(dbLoader);
621
+ this.logger.info("DatabaseLoader configured", { datasource: this.config.datasource, tableName });
622
+ }
216
623
  /**
217
624
  * Register a new metadata loader (data source)
218
625
  */
@@ -1577,12 +1984,14 @@ var MigrationExecutor = class {
1577
1984
  }
1578
1985
  };
1579
1986
  export {
1987
+ DatabaseLoader,
1580
1988
  JSONSerializer,
1581
1989
  MemoryLoader,
1582
1990
  MetadataManager,
1583
1991
  MetadataPlugin,
1584
1992
  migration_exports as Migration,
1585
1993
  RemoteLoader,
1994
+ SysMetadataObject,
1586
1995
  TypeScriptSerializer,
1587
1996
  YAMLSerializer
1588
1997
  };