@objectstack/metadata 4.0.5 → 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/dist/node.cjs CHANGED
@@ -37,7 +37,6 @@ __export(node_exports, {
37
37
  MemoryLoader: () => MemoryLoader,
38
38
  MetadataManager: () => MetadataManager,
39
39
  MetadataPlugin: () => MetadataPlugin,
40
- MetadataProjector: () => MetadataProjector,
41
40
  Migration: () => migration_exports,
42
41
  NodeMetadataManager: () => NodeMetadataManager,
43
42
  RemoteLoader: () => RemoteLoader,
@@ -323,307 +322,113 @@ function generateDiffSummary(diff) {
323
322
  return summary.join(", ");
324
323
  }
325
324
 
326
- // src/projection/metadata-projector.ts
327
- var import_system = require("@objectstack/spec/system");
328
- var MetadataProjector = class {
329
- constructor(options) {
330
- // Map of metadata types to their target table names
331
- this.typeTableMap = {
332
- object: "sys_object",
333
- view: "sys_view",
334
- agent: "sys_agent",
335
- tool: "sys_tool",
336
- flow: "sys_flow"
337
- // Add more as needed: dashboard, app, action, workflow, etc.
338
- };
339
- if (!options.driver && !options.engine) {
340
- throw new Error("MetadataProjector requires either a driver or engine");
341
- }
342
- this.driver = options.driver;
343
- this.engine = options.engine;
344
- this.scope = {
345
- organizationId: options.organizationId,
346
- projectId: options.projectId
347
- };
325
+ // src/utils/lru-cache.ts
326
+ var LRUCache = class {
327
+ constructor(options = {}) {
328
+ this.map = /* @__PURE__ */ new Map();
329
+ this.hits = 0;
330
+ this.misses = 0;
331
+ this.maxSize = options.maxSize && options.maxSize > 0 ? options.maxSize : 0;
332
+ this.ttl = options.ttl && options.ttl > 0 ? options.ttl : 0;
333
+ }
334
+ get(key) {
335
+ const entry = this.map.get(key);
336
+ if (!entry) {
337
+ this.misses++;
338
+ return void 0;
339
+ }
340
+ if (entry.expiresAt !== 0 && entry.expiresAt <= Date.now()) {
341
+ this.map.delete(key);
342
+ this.misses++;
343
+ return void 0;
344
+ }
345
+ this.map.delete(key);
346
+ this.map.set(key, entry);
347
+ this.hits++;
348
+ return entry.value;
349
+ }
350
+ set(key, value) {
351
+ if (this.map.has(key)) {
352
+ this.map.delete(key);
353
+ } else if (this.maxSize > 0 && this.map.size >= this.maxSize) {
354
+ const oldest = this.map.keys().next();
355
+ if (!oldest.done) this.map.delete(oldest.value);
356
+ }
357
+ this.map.set(key, {
358
+ value,
359
+ expiresAt: this.ttl > 0 ? Date.now() + this.ttl : 0
360
+ });
348
361
  }
349
- /**
350
- * Project metadata to type-specific table
351
- */
352
- async project(type, name, data) {
353
- const targetTable = this.typeTableMap[type];
354
- if (!targetTable) {
355
- return;
356
- }
357
- const projectedData = this.transformToProjection(type, name, data);
358
- if (!projectedData) {
359
- return;
360
- }
361
- try {
362
- const projId = this.scope.projectId ?? null;
363
- const existing = await this._findOne(targetTable, {
364
- where: { name, project_id: projId }
365
- });
366
- if (existing) {
367
- await this._update(targetTable, existing.id, projectedData);
368
- } else {
369
- const id = this.generateId();
370
- await this._create(targetTable, {
371
- id,
372
- ...projectedData
373
- });
374
- }
375
- } catch (error) {
376
- console.error(`Failed to project ${type}/${name} to ${targetTable}:`, error);
377
- }
362
+ has(key) {
363
+ return this.get(key) !== void 0;
378
364
  }
379
- /**
380
- * Delete projection from type-specific table
381
- */
382
- async deleteProjection(type, name) {
383
- const targetTable = this.typeTableMap[type];
384
- if (!targetTable) {
385
- return;
386
- }
387
- try {
388
- const projId = this.scope.projectId ?? null;
389
- const existing = await this._findOne(targetTable, {
390
- where: { name, project_id: projId }
391
- });
392
- if (existing) {
393
- await this._delete(targetTable, existing.id);
394
- }
395
- } catch (error) {
396
- console.error(`Failed to delete projection ${type}/${name} from ${targetTable}:`, error);
397
- }
365
+ delete(key) {
366
+ return this.map.delete(key);
398
367
  }
399
- /**
400
- * Transform metadata into projection record
401
- */
402
- transformToProjection(type, name, data) {
403
- const now = (/* @__PURE__ */ new Date()).toISOString();
404
- switch (type) {
405
- case "object":
406
- return this.projectObject(name, data, now);
407
- case "view":
408
- return this.projectView(name, data, now);
409
- case "agent":
410
- return this.projectAgent(name, data, now);
411
- case "tool":
412
- return this.projectTool(name, data, now);
413
- case "flow":
414
- return this.projectFlow(name, data, now);
415
- default:
416
- return null;
417
- }
368
+ clear() {
369
+ this.map.clear();
418
370
  }
419
- /**
420
- * Project object metadata to sys_object
421
- */
422
- projectObject(name, data, now) {
423
- return {
424
- name,
425
- project_id: this.scope.projectId ?? null,
426
- label: data.label || name,
427
- plural_label: data.pluralLabel || data.label || name,
428
- description: data.description || "",
429
- icon: data.icon || "database",
430
- namespace: data.namespace || "default",
431
- tags: Array.isArray(data.tags) ? data.tags.join(",") : data.tags || "",
432
- active: data.active !== false,
433
- is_system: data.isSystem || false,
434
- abstract: data.abstract || false,
435
- datasource: data.datasource || "default",
436
- table_name: data.name ? import_system.StorageNameMapping.resolveTableName({ name: data.name }) : name,
437
- // Serialize complex structures as JSON
438
- fields_json: data.fields ? JSON.stringify(data.fields) : null,
439
- indexes_json: data.indexes ? JSON.stringify(data.indexes) : null,
440
- validations_json: data.validations ? JSON.stringify(data.validations) : null,
441
- state_machines_json: data.stateMachines ? JSON.stringify(data.stateMachines) : null,
442
- capabilities_json: data.enable ? JSON.stringify(data.enable) : null,
443
- // Denormalized fields
444
- field_count: data.fields ? Object.keys(data.fields).length : 0,
445
- display_name_field: data.displayNameField || null,
446
- title_format: data.titleFormat || null,
447
- compact_layout: Array.isArray(data.compactLayout) ? data.compactLayout.join(",") : data.compactLayout || null,
448
- // Capabilities (denormalized for easier querying)
449
- track_history: data.enable?.trackHistory || false,
450
- searchable: data.enable?.searchable !== false,
451
- api_enabled: data.enable?.apiEnabled !== false,
452
- files: data.enable?.files || false,
453
- feeds: data.enable?.feeds || false,
454
- activities: data.enable?.activities || false,
455
- trash: data.enable?.trash !== false,
456
- mru: data.enable?.mru !== false,
457
- clone: data.enable?.clone !== false,
458
- // Package management
459
- package_id: data.packageId || null,
460
- managed_by: data.managedBy || "user",
461
- // Audit
462
- created_by: data.createdBy || null,
463
- created_at: data.createdAt || now,
464
- updated_by: data.updatedBy || null,
465
- updated_at: now
466
- };
371
+ get size() {
372
+ return this.map.size;
467
373
  }
468
- /**
469
- * Project view metadata to sys_view
470
- */
471
- projectView(name, data, now) {
374
+ /** Diagnostic counters — useful for `metrics` endpoints. */
375
+ stats() {
376
+ const total = this.hits + this.misses;
472
377
  return {
473
- name,
474
- project_id: this.scope.projectId ?? null,
475
- label: data.label || name,
476
- description: data.description || "",
477
- object_name: data.object || "",
478
- view_type: data.type || "grid",
479
- // Serialize configurations as JSON
480
- columns_json: data.columns ? JSON.stringify(data.columns) : null,
481
- filters_json: data.filters ? JSON.stringify(data.filters) : null,
482
- sort_json: data.sort ? JSON.stringify(data.sort) : null,
483
- config_json: data.config ? JSON.stringify(data.config) : null,
484
- // Display options
485
- page_size: data.pageSize || 25,
486
- show_search: data.showSearch !== false,
487
- show_filters: data.showFilters !== false,
488
- // Classification
489
- namespace: data.namespace || "default",
490
- // Package management
491
- package_id: data.packageId || null,
492
- managed_by: data.managedBy || "user",
493
- // Audit
494
- created_by: data.createdBy || null,
495
- created_at: data.createdAt || now,
496
- updated_by: data.updatedBy || null,
497
- updated_at: now
378
+ size: this.map.size,
379
+ hits: this.hits,
380
+ misses: this.misses,
381
+ hitRate: total === 0 ? 0 : this.hits / total
498
382
  };
499
383
  }
500
- /**
501
- * Project agent metadata to sys_agent
502
- */
503
- projectAgent(name, data, now) {
504
- return {
505
- name,
506
- project_id: this.scope.projectId ?? null,
507
- label: data.label || name,
508
- description: data.description || "",
509
- agent_type: data.type || "conversational",
510
- // Model configuration
511
- model: data.model || null,
512
- temperature: data.temperature ?? 0.7,
513
- max_tokens: data.maxTokens || null,
514
- top_p: data.topP || null,
515
- // System prompt
516
- system_prompt: data.systemPrompt || null,
517
- // Tools and skills as JSON
518
- tools_json: data.tools ? JSON.stringify(data.tools) : null,
519
- skills_json: data.skills ? JSON.stringify(data.skills) : null,
520
- // Memory
521
- memory_enabled: data.memoryEnabled || false,
522
- memory_window: data.memoryWindow || 10,
523
- // Classification
524
- namespace: data.namespace || "default",
525
- // Package management
526
- package_id: data.packageId || null,
527
- managed_by: data.managedBy || "user",
528
- // Audit
529
- created_by: data.createdBy || null,
530
- created_at: data.createdAt || now,
531
- updated_by: data.updatedBy || null,
532
- updated_at: now
533
- };
384
+ /** Resets hit/miss counters without dropping cached entries. */
385
+ resetStats() {
386
+ this.hits = 0;
387
+ this.misses = 0;
534
388
  }
535
- /**
536
- * Project tool metadata to sys_tool
537
- */
538
- projectTool(name, data, now) {
539
- return {
540
- name,
541
- project_id: this.scope.projectId ?? null,
542
- label: data.label || name,
543
- description: data.description || "",
544
- // Parameters and implementation
545
- parameters_json: data.parameters ? JSON.stringify(data.parameters) : null,
546
- handler_code: data.handler || null,
547
- // Classification
548
- namespace: data.namespace || "default",
549
- // Package management
550
- package_id: data.packageId || null,
551
- managed_by: data.managedBy || "user",
552
- // Audit
553
- created_by: data.createdBy || null,
554
- created_at: data.createdAt || now,
555
- updated_by: data.updatedBy || null,
556
- updated_at: now
557
- };
558
- }
559
- /**
560
- * Project flow metadata to sys_flow
561
- */
562
- projectFlow(name, data, now) {
563
- return {
564
- name,
565
- project_id: this.scope.projectId ?? null,
566
- label: data.label || name,
567
- description: data.description || "",
568
- flow_type: data.type || "autolaunched",
569
- // Flow definition
570
- nodes_json: data.nodes ? JSON.stringify(data.nodes) : null,
571
- edges_json: data.edges ? JSON.stringify(data.edges) : null,
572
- variables_json: data.variables ? JSON.stringify(data.variables) : null,
573
- // Trigger configuration
574
- trigger_type: data.triggerType || null,
575
- trigger_object: data.triggerObject || null,
576
- // Status
577
- active: data.active || false,
578
- // Classification
579
- namespace: data.namespace || "default",
580
- // Package management
581
- package_id: data.packageId || null,
582
- managed_by: data.managedBy || "user",
583
- // Audit
584
- created_by: data.createdBy || null,
585
- created_at: data.createdAt || now,
586
- updated_by: data.updatedBy || null,
587
- updated_at: now
588
- };
589
- }
590
- // ==========================================
591
- // Internal CRUD helpers (driver vs engine)
592
- // ==========================================
593
- async _findOne(table, query) {
594
- if (this.engine) {
595
- return this.engine.findOne(table, query);
596
- }
597
- return this.driver.findOne(table, { object: table, ...query });
598
- }
599
- async _create(table, data) {
600
- if (this.engine) {
601
- return this.engine.insert(table, data);
602
- }
603
- return this.driver.create(table, data);
604
- }
605
- async _update(table, id, data) {
606
- if (this.engine) {
607
- return this.engine.update(table, { id, ...data });
389
+ };
390
+
391
+ // src/migrations/add-sys-metadata-overlay-index.ts
392
+ var INDEX_NAME = "idx_sys_metadata_overlay_active";
393
+ var TABLE = "sys_metadata";
394
+ var COLUMNS = "(type, name, organization_id, project_id, scope)";
395
+ var WHERE = "state = 'active'";
396
+ async function addSysMetadataOverlayIndex(driver) {
397
+ const driverAny = driver;
398
+ const exec = async (sql) => {
399
+ if (typeof driverAny.raw === "function") {
400
+ await driverAny.raw(sql);
401
+ } else if (typeof driverAny.execute === "function") {
402
+ await driverAny.execute(sql);
403
+ } else {
404
+ throw new Error("driver has neither raw nor execute");
405
+ }
406
+ };
407
+ const partialSql = `CREATE UNIQUE INDEX IF NOT EXISTS ${INDEX_NAME} ON ${TABLE} ${COLUMNS} WHERE ${WHERE}`;
408
+ const fallbackSql = `CREATE INDEX IF NOT EXISTS ${INDEX_NAME} ON ${TABLE} ${COLUMNS}`;
409
+ try {
410
+ await exec(partialSql);
411
+ return { index: INDEX_NAME, status: "created" };
412
+ } catch (err) {
413
+ const msg = err instanceof Error ? err.message : String(err);
414
+ if (/partial|where clause|syntax/i.test(msg)) {
415
+ try {
416
+ await exec(fallbackSql);
417
+ return { index: INDEX_NAME, status: "fallback_non_unique" };
418
+ } catch (fallbackErr) {
419
+ return {
420
+ index: INDEX_NAME,
421
+ status: "error",
422
+ error: fallbackErr instanceof Error ? fallbackErr.message : String(fallbackErr)
423
+ };
424
+ }
608
425
  }
609
- return this.driver.update(table, id, data);
610
- }
611
- async _delete(table, id) {
612
- if (this.engine) {
613
- return this.engine.delete(table, { where: { id } });
426
+ if (/already exists/i.test(msg)) {
427
+ return { index: INDEX_NAME, status: "already_exists" };
614
428
  }
615
- return this.driver.delete(table, id);
429
+ return { index: INDEX_NAME, status: "error", error: msg };
616
430
  }
617
- /**
618
- * Generate a simple unique ID
619
- */
620
- generateId() {
621
- if (typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.randomUUID === "function") {
622
- return globalThis.crypto.randomUUID();
623
- }
624
- return `proj_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
625
- }
626
- };
431
+ }
627
432
 
628
433
  // src/loaders/database-loader.ts
629
434
  var DatabaseLoader = class {
@@ -650,17 +455,56 @@ var DatabaseLoader = class {
650
455
  this.organizationId = options.organizationId;
651
456
  this.projectId = options.projectId;
652
457
  this.trackHistory = options.trackHistory !== false;
653
- this.enableProjection = options.enableProjection !== false;
654
- if (this.enableProjection) {
655
- this.projector = new MetadataProjector({
656
- driver: this.driver,
657
- engine: this.engine,
658
- organizationId: this.organizationId,
659
- projectId: this.projectId
660
- });
458
+ const cacheOpts = options.cache;
459
+ const cacheEnabled = cacheOpts?.enabled !== false;
460
+ if (cacheEnabled) {
461
+ const lruOpts = {
462
+ maxSize: cacheOpts?.maxSize ?? 500,
463
+ ttl: cacheOpts?.ttl ?? 6e4
464
+ };
465
+ this.loadCache = new LRUCache(lruOpts);
466
+ this.loadManyCache = new LRUCache(lruOpts);
467
+ this.listCache = new LRUCache(lruOpts);
468
+ this.statCache = new LRUCache(lruOpts);
661
469
  }
662
470
  }
663
471
  // ==========================================
472
+ // Cache helpers
473
+ // ==========================================
474
+ cacheKey(type, name) {
475
+ return `${type}::${name}`;
476
+ }
477
+ /**
478
+ * Invalidate all cached entries for a specific (type, name) pair plus
479
+ * the type-level aggregates (`loadMany`, `list`). Called from every write
480
+ * path (`save`, `delete`, `registerRollback`).
481
+ */
482
+ invalidate(type, name) {
483
+ if (!this.loadCache) return;
484
+ const key = this.cacheKey(type, name);
485
+ this.loadCache.delete(key);
486
+ this.statCache?.delete(key);
487
+ this.loadManyCache?.delete(type);
488
+ this.listCache?.delete(type);
489
+ }
490
+ /** Drop the entire cache — useful after bulk imports or schema changes. */
491
+ invalidateAll() {
492
+ this.loadCache?.clear();
493
+ this.loadManyCache?.clear();
494
+ this.listCache?.clear();
495
+ this.statCache?.clear();
496
+ }
497
+ /** Diagnostic: aggregated cache statistics for `metrics` endpoints. */
498
+ getCacheStats() {
499
+ return {
500
+ enabled: this.loadCache !== void 0,
501
+ load: this.loadCache?.stats() ?? null,
502
+ loadMany: this.loadManyCache?.stats() ?? null,
503
+ list: this.listCache?.stats() ?? null,
504
+ stat: this.statCache?.stats() ?? null
505
+ };
506
+ }
507
+ // ==========================================
664
508
  // Internal CRUD helpers (driver vs engine)
665
509
  // ==========================================
666
510
  async _find(table, query) {
@@ -708,6 +552,23 @@ var DatabaseLoader = class {
708
552
  if (this.schemaReady) return;
709
553
  if (this.engine) {
710
554
  this.schemaReady = true;
555
+ try {
556
+ const engineAny = this.engine;
557
+ let driver = engineAny?.driver ?? engineAny?.getDriver?.();
558
+ if (!driver && engineAny?.drivers instanceof Map) {
559
+ for (const candidate of engineAny.drivers.values()) {
560
+ const c = candidate;
561
+ if (c && (typeof c.raw === "function" || typeof c.execute === "function")) {
562
+ driver = candidate;
563
+ break;
564
+ }
565
+ }
566
+ }
567
+ if (driver) {
568
+ await addSysMetadataOverlayIndex(driver);
569
+ }
570
+ } catch {
571
+ }
711
572
  return;
712
573
  }
713
574
  try {
@@ -716,6 +577,10 @@ var DatabaseLoader = class {
716
577
  name: this.tableName
717
578
  });
718
579
  this.schemaReady = true;
580
+ try {
581
+ await addSysMetadataOverlayIndex(this.driver);
582
+ } catch {
583
+ }
719
584
  } catch {
720
585
  this.schemaReady = true;
721
586
  }
@@ -860,11 +725,24 @@ var DatabaseLoader = class {
860
725
  async load(type, name, _options) {
861
726
  const startTime = Date.now();
862
727
  await this.ensureSchema();
728
+ const key = this.cacheKey(type, name);
729
+ if (this.loadCache) {
730
+ const cached = this.loadCache.get(key);
731
+ if (cached !== void 0) {
732
+ return {
733
+ data: cached,
734
+ source: "database",
735
+ format: "json",
736
+ loadTime: Date.now() - startTime
737
+ };
738
+ }
739
+ }
863
740
  try {
864
741
  const row = await this._findOne(this.tableName, {
865
742
  where: this.baseFilter(type, name)
866
743
  });
867
744
  if (!row) {
745
+ this.loadCache?.set(key, null);
868
746
  return {
869
747
  data: null,
870
748
  loadTime: Date.now() - startTime
@@ -872,6 +750,7 @@ var DatabaseLoader = class {
872
750
  }
873
751
  const data = this.rowToData(row);
874
752
  const record = this.rowToRecord(row);
753
+ this.loadCache?.set(key, data);
875
754
  return {
876
755
  data,
877
756
  source: "database",
@@ -888,17 +767,27 @@ var DatabaseLoader = class {
888
767
  }
889
768
  async loadMany(type, _options) {
890
769
  await this.ensureSchema();
770
+ if (this.loadManyCache) {
771
+ const cached = this.loadManyCache.get(type);
772
+ if (cached !== void 0) return cached;
773
+ }
891
774
  try {
892
775
  const rows = await this._find(this.tableName, {
893
776
  where: this.baseFilter(type)
894
777
  });
895
- return rows.map((row) => this.rowToData(row)).filter((data) => data !== null);
778
+ const result = rows.map((row) => this.rowToData(row)).filter((data) => data !== null);
779
+ this.loadManyCache?.set(type, result);
780
+ return result;
896
781
  } catch {
897
782
  return [];
898
783
  }
899
784
  }
900
785
  async exists(type, name) {
901
786
  await this.ensureSchema();
787
+ if (this.loadCache) {
788
+ const cached = this.loadCache.get(this.cacheKey(type, name));
789
+ if (cached !== void 0) return cached !== null;
790
+ }
902
791
  try {
903
792
  const count = await this._count(this.tableName, {
904
793
  where: this.baseFilter(type, name)
@@ -910,31 +799,47 @@ var DatabaseLoader = class {
910
799
  }
911
800
  async stat(type, name) {
912
801
  await this.ensureSchema();
802
+ const key = this.cacheKey(type, name);
803
+ if (this.statCache) {
804
+ const cached = this.statCache.get(key);
805
+ if (cached !== void 0) return cached;
806
+ }
913
807
  try {
914
808
  const row = await this._findOne(this.tableName, {
915
809
  where: this.baseFilter(type, name)
916
810
  });
917
- if (!row) return null;
811
+ if (!row) {
812
+ this.statCache?.set(key, null);
813
+ return null;
814
+ }
918
815
  const record = this.rowToRecord(row);
919
816
  const metadataStr = typeof row.metadata === "string" ? row.metadata : JSON.stringify(row.metadata);
920
- return {
817
+ const stats = {
921
818
  size: metadataStr.length,
922
819
  mtime: record.updatedAt ?? record.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
923
820
  format: "json",
924
821
  etag: record.checksum
925
822
  };
823
+ this.statCache?.set(key, stats);
824
+ return stats;
926
825
  } catch {
927
826
  return null;
928
827
  }
929
828
  }
930
829
  async list(type) {
931
830
  await this.ensureSchema();
831
+ if (this.listCache) {
832
+ const cached = this.listCache.get(type);
833
+ if (cached !== void 0) return cached;
834
+ }
932
835
  try {
933
836
  const rows = await this._find(this.tableName, {
934
837
  where: this.baseFilter(type),
935
838
  fields: ["name"]
936
839
  });
937
- return rows.map((row) => row.name).filter((name) => typeof name === "string");
840
+ const names = rows.map((row) => row.name).filter((name) => typeof name === "string");
841
+ this.listCache?.set(type, names);
842
+ return names;
938
843
  } catch {
939
844
  return [];
940
845
  }
@@ -1074,6 +979,7 @@ var DatabaseLoader = class {
1074
979
  updated_at: now,
1075
980
  state: "active"
1076
981
  });
982
+ this.invalidate(type, name);
1077
983
  await this.createHistoryRecord(
1078
984
  existing.id,
1079
985
  type,
@@ -1099,6 +1005,7 @@ var DatabaseLoader = class {
1099
1005
  if (existing) {
1100
1006
  const previousChecksum = existing.checksum;
1101
1007
  if (newChecksum === previousChecksum) {
1008
+ this.loadCache?.set(this.cacheKey(type, name), data);
1102
1009
  return {
1103
1010
  success: true,
1104
1011
  path: `datasource://${this.tableName}/${type}/${name}`,
@@ -1114,6 +1021,7 @@ var DatabaseLoader = class {
1114
1021
  updated_at: now,
1115
1022
  state: "active"
1116
1023
  });
1024
+ this.invalidate(type, name);
1117
1025
  await this.createHistoryRecord(
1118
1026
  existing.id,
1119
1027
  type,
@@ -1123,9 +1031,6 @@ var DatabaseLoader = class {
1123
1031
  "update",
1124
1032
  previousChecksum
1125
1033
  );
1126
- if (this.projector) {
1127
- await this.projector.project(type, name, data);
1128
- }
1129
1034
  return {
1130
1035
  success: true,
1131
1036
  path: `datasource://${this.tableName}/${type}/${name}`,
@@ -1151,6 +1056,7 @@ var DatabaseLoader = class {
1151
1056
  created_at: now,
1152
1057
  updated_at: now
1153
1058
  });
1059
+ this.invalidate(type, name);
1154
1060
  await this.createHistoryRecord(
1155
1061
  id,
1156
1062
  type,
@@ -1159,9 +1065,6 @@ var DatabaseLoader = class {
1159
1065
  data,
1160
1066
  "create"
1161
1067
  );
1162
- if (this.projector) {
1163
- await this.projector.project(type, name, data);
1164
- }
1165
1068
  return {
1166
1069
  success: true,
1167
1070
  path: `datasource://${this.tableName}/${type}/${name}`,
@@ -1187,9 +1090,7 @@ var DatabaseLoader = class {
1187
1090
  return;
1188
1091
  }
1189
1092
  await this._delete(this.tableName, existing.id);
1190
- if (this.projector) {
1191
- await this.projector.deleteProjection(type, name);
1192
- }
1093
+ this.invalidate(type, name);
1193
1094
  }
1194
1095
  };
1195
1096
  function generateId() {
@@ -1200,7 +1101,7 @@ function generateId() {
1200
1101
  }
1201
1102
 
1202
1103
  // src/metadata-manager.ts
1203
- var MetadataManager = class {
1104
+ var _MetadataManager = class _MetadataManager {
1204
1105
  constructor(config) {
1205
1106
  this.loaders = /* @__PURE__ */ new Map();
1206
1107
  this.watchCallbacks = /* @__PURE__ */ new Map();
@@ -1212,6 +1113,18 @@ var MetadataManager = class {
1212
1113
  this.typeRegistry = [];
1213
1114
  // Dependency tracking: "type:name" -> dependencies
1214
1115
  this.dependencies = /* @__PURE__ */ new Map();
1116
+ // Short-lived cache for list() results. Built primarily to break the
1117
+ // deadlock that occurs when security/permission middleware calls
1118
+ // `list('permission')` from inside a user-initiated DB transaction: the
1119
+ // DatabaseLoader's `engine.find('sys_metadata', ...)` would then try to
1120
+ // acquire a fresh knex connection while the transaction is still holding
1121
+ // SQLite's single connection — knex waits the full `acquireConnectionTimeout`
1122
+ // (60s) before returning []. The cache absorbs the repeated lookups so the
1123
+ // loader is only hit once per TTL window.
1124
+ //
1125
+ // Invalidated on every `register()` / `unregister()` to keep CRUD writes
1126
+ // visible to subsequent reads.
1127
+ this.listCache = /* @__PURE__ */ new Map();
1215
1128
  this.config = config;
1216
1129
  this.logger = (0, import_core.createLogger)({ level: "info", format: "pretty" });
1217
1130
  this.serializers = /* @__PURE__ */ new Map();
@@ -1262,7 +1175,8 @@ var MetadataManager = class {
1262
1175
  driver,
1263
1176
  tableName,
1264
1177
  organizationId,
1265
- projectId
1178
+ projectId,
1179
+ cache: this.config.cache?.databaseLoader
1266
1180
  });
1267
1181
  this.registerLoader(dbLoader);
1268
1182
  this.logger.info("DatabaseLoader configured", { datasource: this.config.datasource, tableName });
@@ -1290,7 +1204,8 @@ var MetadataManager = class {
1290
1204
  engine,
1291
1205
  tableName,
1292
1206
  organizationId,
1293
- projectId
1207
+ projectId,
1208
+ cache: this.config.cache?.databaseLoader
1294
1209
  });
1295
1210
  this.registerLoader(dbLoader);
1296
1211
  this.logger.info("DatabaseLoader configured via DataEngine", { tableName });
@@ -1322,10 +1237,19 @@ var MetadataManager = class {
1322
1237
  * should not be written to during runtime registration.
1323
1238
  */
1324
1239
  async register(type, name, data) {
1240
+ if (this.config.persistence?.writable === false) {
1241
+ const msg = `MetadataManager is read-only (persistence.writable=false); refusing to register ${type}/${name}`;
1242
+ if (this.config.validation?.throwOnError) {
1243
+ throw new Error(msg);
1244
+ }
1245
+ this.logger.warn(msg);
1246
+ return;
1247
+ }
1325
1248
  if (!this.registry.has(type)) {
1326
1249
  this.registry.set(type, /* @__PURE__ */ new Map());
1327
1250
  }
1328
1251
  this.registry.get(type).set(name, data);
1252
+ this.invalidateListCache(type);
1329
1253
  for (const loader of this.loaders.values()) {
1330
1254
  if (loader.save && loader.contract.protocol === "datasource:" && loader.contract.capabilities.write) {
1331
1255
  await loader.save(type, name, data);
@@ -1367,6 +1291,10 @@ var MetadataManager = class {
1367
1291
  * List all metadata items of a given type
1368
1292
  */
1369
1293
  async list(type) {
1294
+ const cached = this.listCache.get(type);
1295
+ if (cached && Date.now() - cached.ts < _MetadataManager.LIST_CACHE_TTL_MS) {
1296
+ return cached.items;
1297
+ }
1370
1298
  const items = /* @__PURE__ */ new Map();
1371
1299
  const typeStore = this.registry.get(type);
1372
1300
  if (typeStore) {
@@ -1387,7 +1315,16 @@ var MetadataManager = class {
1387
1315
  this.logger.warn(`Loader ${loader.contract.name} failed to loadMany ${type}`, { error: e });
1388
1316
  }
1389
1317
  }
1390
- return Array.from(items.values());
1318
+ const result = Array.from(items.values());
1319
+ this.cacheListResult(type, result);
1320
+ return result;
1321
+ }
1322
+ cacheListResult(type, items) {
1323
+ this.listCache.set(type, { ts: Date.now(), items });
1324
+ }
1325
+ /** Internal helper: drop the cached `list()` result for a type. */
1326
+ invalidateListCache(type) {
1327
+ this.listCache.delete(type);
1391
1328
  }
1392
1329
  /**
1393
1330
  * Unregister/remove a metadata item by type and name.
@@ -1401,6 +1338,7 @@ var MetadataManager = class {
1401
1338
  this.registry.delete(type);
1402
1339
  }
1403
1340
  }
1341
+ this.invalidateListCache(type);
1404
1342
  for (const loader of this.loaders.values()) {
1405
1343
  if (loader.contract.protocol !== "datasource:" || !loader.contract.capabilities.write) continue;
1406
1344
  if (typeof loader.delete === "function") {
@@ -1815,6 +1753,14 @@ var MetadataManager = class {
1815
1753
  * Save/update an overlay for a metadata item
1816
1754
  */
1817
1755
  async saveOverlay(overlay) {
1756
+ if (this.config.persistence?.overlayWritable === false) {
1757
+ const msg = `MetadataManager overlays are read-only (persistence.overlayWritable=false); refusing to save overlay for ${overlay.baseType}/${overlay.baseName}`;
1758
+ if (this.config.validation?.throwOnError) {
1759
+ throw new Error(msg);
1760
+ }
1761
+ this.logger.warn(msg);
1762
+ return;
1763
+ }
1818
1764
  const key = this.overlayKey(overlay.baseType, overlay.baseName, overlay.scope);
1819
1765
  this.overlays.set(key, overlay);
1820
1766
  }
@@ -2284,6 +2230,8 @@ var MetadataManager = class {
2284
2230
  };
2285
2231
  }
2286
2232
  };
2233
+ _MetadataManager.LIST_CACHE_TTL_MS = 3e4;
2234
+ var MetadataManager = _MetadataManager;
2287
2235
 
2288
2236
  // src/plugin.ts
2289
2237
  var import_promises = require("fs/promises");
@@ -2775,7 +2723,10 @@ var MemoryLoader = class {
2775
2723
  // src/plugin.ts
2776
2724
  var import_kernel = require("@objectstack/spec/kernel");
2777
2725
  var import_metadata2 = require("@objectstack/platform-objects/metadata");
2778
- var queryableMetadataObjects = [import_metadata2.SysObject, import_metadata2.SysView, import_metadata2.SysFlow, import_metadata2.SysAgent, import_metadata2.SysTool];
2726
+ var queryableMetadataObjects = [
2727
+ import_metadata2.SysMetadataObject,
2728
+ import_metadata2.SysMetadataHistoryObject
2729
+ ];
2779
2730
  var ARTIFACT_FIELD_TO_TYPE = {
2780
2731
  objects: "object",
2781
2732
  objectExtensions: "object_extension",
@@ -2843,13 +2794,35 @@ var MetadataPlugin = class {
2843
2794
  };
2844
2795
  this.start = async (ctx) => {
2845
2796
  const src = this.options.artifactSource;
2846
- if (src?.mode === "local-file") {
2847
- await this._loadFromLocalFile(ctx, src.path);
2848
- } else if (src?.mode === "artifact-api") {
2849
- ctx.logger.warn("[MetadataPlugin] artifact-api source is not yet implemented; falling back to file-system scan");
2850
- await this._loadFromFileSystem(ctx);
2797
+ const mode = this.options.config?.bootstrap ?? "eager";
2798
+ ctx.logger.info("[MetadataPlugin] Bootstrapping metadata", {
2799
+ bootstrap: mode,
2800
+ artifactSource: src?.mode ?? "none"
2801
+ });
2802
+ if (mode === "artifact-only") {
2803
+ if (src?.mode === "local-file") {
2804
+ await this._loadFromLocalFile(ctx, src.path, src.fetchTimeoutMs);
2805
+ } else if (src?.mode === "artifact-api") {
2806
+ await this._loadFromArtifactApi(ctx, src);
2807
+ } else {
2808
+ throw new Error("[MetadataPlugin] bootstrap=artifact-only requires options.artifactSource to be set");
2809
+ }
2810
+ } else if (mode === "lazy") {
2811
+ if (src?.mode === "local-file") {
2812
+ await this._loadFromLocalFile(ctx, src.path, src.fetchTimeoutMs);
2813
+ } else if (src?.mode === "artifact-api") {
2814
+ await this._loadFromArtifactApi(ctx, src);
2815
+ } else {
2816
+ ctx.logger.info("[MetadataPlugin] lazy bootstrap \u2014 skipping filesystem priming; metadata loads on demand");
2817
+ }
2851
2818
  } else {
2852
- await this._loadFromFileSystem(ctx);
2819
+ if (src?.mode === "local-file") {
2820
+ await this._loadFromLocalFile(ctx, src.path, src.fetchTimeoutMs);
2821
+ } else if (src?.mode === "artifact-api") {
2822
+ await this._loadFromArtifactApi(ctx, src);
2823
+ } else {
2824
+ await this._loadFromFileSystem(ctx);
2825
+ }
2853
2826
  }
2854
2827
  try {
2855
2828
  const realtimeService = ctx.getService("realtime");
@@ -2868,45 +2841,46 @@ var MetadataPlugin = class {
2868
2841
  ...options
2869
2842
  };
2870
2843
  const rootDir = this.options.rootDir || process.cwd();
2844
+ const bootstrapMode = this.options.config?.bootstrap ?? "eager";
2845
+ const effectiveWatch = bootstrapMode === "artifact-only" ? false : this.options.watch ?? true;
2871
2846
  this.manager = new NodeMetadataManager({
2872
2847
  rootDir,
2873
- watch: this.options.watch ?? true,
2848
+ watch: effectiveWatch,
2874
2849
  formats: ["yaml", "json", "typescript", "javascript"]
2875
2850
  });
2876
2851
  this.manager.setTypeRegistry(import_kernel.DEFAULT_METADATA_TYPE_REGISTRY);
2877
2852
  }
2878
- async _loadFromLocalFile(ctx, filePath) {
2879
- const isUrl = /^https?:\/\//i.test(filePath);
2880
- ctx.logger.info(
2881
- `[MetadataPlugin] Loading metadata from ${isUrl ? "remote URL" : "local artifact file"}`,
2882
- { path: filePath }
2883
- );
2884
- let raw;
2853
+ /**
2854
+ * Fetch JSON content from a URL with configurable timeout.
2855
+ */
2856
+ async _fetchJson(url, fetchTimeoutMs, token) {
2857
+ const envTimeout = Number(process.env.OS_ARTIFACT_FETCH_TIMEOUT_MS);
2858
+ const timeoutMs = fetchTimeoutMs ?? (Number.isFinite(envTimeout) && envTimeout > 0 ? envTimeout : void 0) ?? 6e4;
2859
+ const controller = new AbortController();
2860
+ const timer = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : void 0;
2885
2861
  try {
2886
- let content;
2887
- if (isUrl) {
2888
- const controller = new AbortController();
2889
- const timer = setTimeout(() => controller.abort(), 15e3);
2890
- try {
2891
- const res = await fetch(filePath, {
2892
- redirect: "follow",
2893
- signal: controller.signal,
2894
- headers: { Accept: "application/json, */*;q=0.5" }
2895
- });
2896
- if (!res.ok) {
2897
- throw new Error(`HTTP ${res.status} ${res.statusText}`);
2898
- }
2899
- content = await res.text();
2900
- } finally {
2901
- clearTimeout(timer);
2902
- }
2903
- } else {
2904
- content = await (0, import_promises.readFile)(filePath, "utf8");
2905
- }
2906
- raw = JSON.parse(content);
2862
+ const headers = { Accept: "application/json, */*;q=0.5" };
2863
+ if (token) headers.Authorization = `Bearer ${token}`;
2864
+ const res = await fetch(url, { redirect: "follow", signal: controller.signal, headers });
2865
+ if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
2866
+ const content = await res.text();
2867
+ return JSON.parse(content);
2907
2868
  } catch (e) {
2908
- throw new Error(`[MetadataPlugin] Cannot read artifact ${isUrl ? "URL" : "file"} at "${filePath}": ${e.message}`);
2869
+ if (e?.name === "AbortError") {
2870
+ throw new Error(
2871
+ `fetch timed out after ${timeoutMs}ms \u2014 set artifactSource.fetchTimeoutMs or OS_ARTIFACT_FETCH_TIMEOUT_MS to extend it (0 disables)`
2872
+ );
2873
+ }
2874
+ throw e;
2875
+ } finally {
2876
+ if (timer) clearTimeout(timer);
2909
2877
  }
2878
+ }
2879
+ /**
2880
+ * Parse raw artifact JSON (envelope or bare definition) and register all
2881
+ * metadata items into the MetadataManager.
2882
+ */
2883
+ async _parseAndRegisterArtifact(ctx, raw, label) {
2910
2884
  const { ProjectArtifactSchema } = await import("@objectstack/spec/cloud");
2911
2885
  const { ObjectStackDefinitionSchema } = await import("@objectstack/spec");
2912
2886
  let metadata;
@@ -2914,6 +2888,9 @@ var MetadataPlugin = class {
2914
2888
  if (obj?.schemaVersion && obj?.commitId && obj?.metadata !== void 0) {
2915
2889
  const artifact = ProjectArtifactSchema.parse(obj);
2916
2890
  metadata = artifact.metadata;
2891
+ } else if (obj?.success && obj?.data?.metadata) {
2892
+ const artifact = ProjectArtifactSchema.parse(obj.data);
2893
+ metadata = artifact.metadata;
2917
2894
  } else {
2918
2895
  const def = ObjectStackDefinitionSchema.parse(obj);
2919
2896
  const canonical = JSON.stringify(def, Object.keys(def).sort());
@@ -2946,10 +2923,51 @@ var MetadataPlugin = class {
2946
2923
  }
2947
2924
  }
2948
2925
  this.manager.registerLoader(memLoader);
2949
- ctx.logger.info("[MetadataPlugin] Artifact metadata loaded", {
2950
- path: filePath,
2951
- totalRegistered
2952
- });
2926
+ ctx.logger.info("[MetadataPlugin] Artifact metadata loaded", { source: label, totalRegistered });
2927
+ return totalRegistered;
2928
+ }
2929
+ async _loadFromLocalFile(ctx, filePath, fetchTimeoutMs) {
2930
+ const isUrl = /^https?:\/\//i.test(filePath);
2931
+ ctx.logger.info(
2932
+ `[MetadataPlugin] Loading metadata from ${isUrl ? "remote URL" : "local artifact file"}`,
2933
+ { path: filePath }
2934
+ );
2935
+ let raw;
2936
+ try {
2937
+ if (isUrl) {
2938
+ raw = await this._fetchJson(filePath, fetchTimeoutMs);
2939
+ } else {
2940
+ const content = await (0, import_promises.readFile)(filePath, "utf8");
2941
+ raw = JSON.parse(content);
2942
+ }
2943
+ } catch (e) {
2944
+ throw new Error(`[MetadataPlugin] Cannot read artifact ${isUrl ? "URL" : "file"} at "${filePath}": ${e.message}`);
2945
+ }
2946
+ await this._parseAndRegisterArtifact(ctx, raw, filePath);
2947
+ }
2948
+ /**
2949
+ * P2: Load metadata from the cloud artifact API endpoint.
2950
+ */
2951
+ async _loadFromArtifactApi(ctx, src) {
2952
+ const projectId = this.options.projectId;
2953
+ if (!projectId) {
2954
+ throw new Error("[MetadataPlugin] artifact-api source requires options.projectId to be set");
2955
+ }
2956
+ let artifactUrl = src.url.replace(/\/+$/, "");
2957
+ if (!/\/api\/v\d+\/cloud\/projects\//i.test(artifactUrl)) {
2958
+ artifactUrl = `${artifactUrl}/api/v1/cloud/projects/${projectId}/artifact`;
2959
+ }
2960
+ if (src.commitId) {
2961
+ artifactUrl += `${artifactUrl.includes("?") ? "&" : "?"}commit=${encodeURIComponent(src.commitId)}`;
2962
+ }
2963
+ ctx.logger.info("[MetadataPlugin] Loading metadata from artifact API", { url: artifactUrl });
2964
+ let raw;
2965
+ try {
2966
+ raw = await this._fetchJson(artifactUrl, src.fetchTimeoutMs, src.token);
2967
+ } catch (e) {
2968
+ throw new Error(`[MetadataPlugin] Cannot load artifact from API "${artifactUrl}": ${e.message}`);
2969
+ }
2970
+ await this._parseAndRegisterArtifact(ctx, raw, artifactUrl);
2953
2971
  }
2954
2972
  async _loadFromFileSystem(ctx) {
2955
2973
  ctx.logger.info("Loading metadata from file system...");
@@ -3473,7 +3491,6 @@ var MigrationExecutor = class {
3473
3491
  MemoryLoader,
3474
3492
  MetadataManager,
3475
3493
  MetadataPlugin,
3476
- MetadataProjector,
3477
3494
  Migration,
3478
3495
  NodeMetadataManager,
3479
3496
  RemoteLoader,