@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/index.cjs CHANGED
@@ -36,7 +36,6 @@ __export(index_exports, {
36
36
  MemoryLoader: () => MemoryLoader,
37
37
  MetadataManager: () => MetadataManager,
38
38
  MetadataPlugin: () => MetadataPlugin,
39
- MetadataProjector: () => MetadataProjector,
40
39
  Migration: () => migration_exports,
41
40
  RemoteLoader: () => RemoteLoader,
42
41
  SysMetadataHistoryObject: () => import_metadata3.SysMetadataHistoryObject,
@@ -321,307 +320,113 @@ function generateDiffSummary(diff) {
321
320
  return summary.join(", ");
322
321
  }
323
322
 
324
- // src/projection/metadata-projector.ts
325
- var import_system = require("@objectstack/spec/system");
326
- var MetadataProjector = class {
327
- constructor(options) {
328
- // Map of metadata types to their target table names
329
- this.typeTableMap = {
330
- object: "sys_object",
331
- view: "sys_view",
332
- agent: "sys_agent",
333
- tool: "sys_tool",
334
- flow: "sys_flow"
335
- // Add more as needed: dashboard, app, action, workflow, etc.
336
- };
337
- if (!options.driver && !options.engine) {
338
- throw new Error("MetadataProjector requires either a driver or engine");
339
- }
340
- this.driver = options.driver;
341
- this.engine = options.engine;
342
- this.scope = {
343
- organizationId: options.organizationId,
344
- projectId: options.projectId
345
- };
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
+ });
346
359
  }
347
- /**
348
- * Project metadata to type-specific table
349
- */
350
- async project(type, name, data) {
351
- const targetTable = this.typeTableMap[type];
352
- if (!targetTable) {
353
- return;
354
- }
355
- const projectedData = this.transformToProjection(type, name, data);
356
- if (!projectedData) {
357
- return;
358
- }
359
- try {
360
- const projId = this.scope.projectId ?? null;
361
- const existing = await this._findOne(targetTable, {
362
- where: { name, project_id: projId }
363
- });
364
- if (existing) {
365
- await this._update(targetTable, existing.id, projectedData);
366
- } else {
367
- const id = this.generateId();
368
- await this._create(targetTable, {
369
- id,
370
- ...projectedData
371
- });
372
- }
373
- } catch (error) {
374
- console.error(`Failed to project ${type}/${name} to ${targetTable}:`, error);
375
- }
360
+ has(key) {
361
+ return this.get(key) !== void 0;
376
362
  }
377
- /**
378
- * Delete projection from type-specific table
379
- */
380
- async deleteProjection(type, name) {
381
- const targetTable = this.typeTableMap[type];
382
- if (!targetTable) {
383
- return;
384
- }
385
- try {
386
- const projId = this.scope.projectId ?? null;
387
- const existing = await this._findOne(targetTable, {
388
- where: { name, project_id: projId }
389
- });
390
- if (existing) {
391
- await this._delete(targetTable, existing.id);
392
- }
393
- } catch (error) {
394
- console.error(`Failed to delete projection ${type}/${name} from ${targetTable}:`, error);
395
- }
363
+ delete(key) {
364
+ return this.map.delete(key);
396
365
  }
397
- /**
398
- * Transform metadata into projection record
399
- */
400
- transformToProjection(type, name, data) {
401
- const now = (/* @__PURE__ */ new Date()).toISOString();
402
- switch (type) {
403
- case "object":
404
- return this.projectObject(name, data, now);
405
- case "view":
406
- return this.projectView(name, data, now);
407
- case "agent":
408
- return this.projectAgent(name, data, now);
409
- case "tool":
410
- return this.projectTool(name, data, now);
411
- case "flow":
412
- return this.projectFlow(name, data, now);
413
- default:
414
- return null;
415
- }
366
+ clear() {
367
+ this.map.clear();
416
368
  }
417
- /**
418
- * Project object metadata to sys_object
419
- */
420
- projectObject(name, data, now) {
421
- return {
422
- name,
423
- project_id: this.scope.projectId ?? null,
424
- label: data.label || name,
425
- plural_label: data.pluralLabel || data.label || name,
426
- description: data.description || "",
427
- icon: data.icon || "database",
428
- namespace: data.namespace || "default",
429
- tags: Array.isArray(data.tags) ? data.tags.join(",") : data.tags || "",
430
- active: data.active !== false,
431
- is_system: data.isSystem || false,
432
- abstract: data.abstract || false,
433
- datasource: data.datasource || "default",
434
- table_name: data.name ? import_system.StorageNameMapping.resolveTableName({ name: data.name }) : name,
435
- // Serialize complex structures as JSON
436
- fields_json: data.fields ? JSON.stringify(data.fields) : null,
437
- indexes_json: data.indexes ? JSON.stringify(data.indexes) : null,
438
- validations_json: data.validations ? JSON.stringify(data.validations) : null,
439
- state_machines_json: data.stateMachines ? JSON.stringify(data.stateMachines) : null,
440
- capabilities_json: data.enable ? JSON.stringify(data.enable) : null,
441
- // Denormalized fields
442
- field_count: data.fields ? Object.keys(data.fields).length : 0,
443
- display_name_field: data.displayNameField || null,
444
- title_format: data.titleFormat || null,
445
- compact_layout: Array.isArray(data.compactLayout) ? data.compactLayout.join(",") : data.compactLayout || null,
446
- // Capabilities (denormalized for easier querying)
447
- track_history: data.enable?.trackHistory || false,
448
- searchable: data.enable?.searchable !== false,
449
- api_enabled: data.enable?.apiEnabled !== false,
450
- files: data.enable?.files || false,
451
- feeds: data.enable?.feeds || false,
452
- activities: data.enable?.activities || false,
453
- trash: data.enable?.trash !== false,
454
- mru: data.enable?.mru !== false,
455
- clone: data.enable?.clone !== false,
456
- // Package management
457
- package_id: data.packageId || null,
458
- managed_by: data.managedBy || "user",
459
- // Audit
460
- created_by: data.createdBy || null,
461
- created_at: data.createdAt || now,
462
- updated_by: data.updatedBy || null,
463
- updated_at: now
464
- };
369
+ get size() {
370
+ return this.map.size;
465
371
  }
466
- /**
467
- * Project view metadata to sys_view
468
- */
469
- projectView(name, data, now) {
372
+ /** Diagnostic counters — useful for `metrics` endpoints. */
373
+ stats() {
374
+ const total = this.hits + this.misses;
470
375
  return {
471
- name,
472
- project_id: this.scope.projectId ?? null,
473
- label: data.label || name,
474
- description: data.description || "",
475
- object_name: data.object || "",
476
- view_type: data.type || "grid",
477
- // Serialize configurations as JSON
478
- columns_json: data.columns ? JSON.stringify(data.columns) : null,
479
- filters_json: data.filters ? JSON.stringify(data.filters) : null,
480
- sort_json: data.sort ? JSON.stringify(data.sort) : null,
481
- config_json: data.config ? JSON.stringify(data.config) : null,
482
- // Display options
483
- page_size: data.pageSize || 25,
484
- show_search: data.showSearch !== false,
485
- show_filters: data.showFilters !== false,
486
- // Classification
487
- namespace: data.namespace || "default",
488
- // Package management
489
- package_id: data.packageId || null,
490
- managed_by: data.managedBy || "user",
491
- // Audit
492
- created_by: data.createdBy || null,
493
- created_at: data.createdAt || now,
494
- updated_by: data.updatedBy || null,
495
- updated_at: now
376
+ size: this.map.size,
377
+ hits: this.hits,
378
+ misses: this.misses,
379
+ hitRate: total === 0 ? 0 : this.hits / total
496
380
  };
497
381
  }
498
- /**
499
- * Project agent metadata to sys_agent
500
- */
501
- projectAgent(name, data, now) {
502
- return {
503
- name,
504
- project_id: this.scope.projectId ?? null,
505
- label: data.label || name,
506
- description: data.description || "",
507
- agent_type: data.type || "conversational",
508
- // Model configuration
509
- model: data.model || null,
510
- temperature: data.temperature ?? 0.7,
511
- max_tokens: data.maxTokens || null,
512
- top_p: data.topP || null,
513
- // System prompt
514
- system_prompt: data.systemPrompt || null,
515
- // Tools and skills as JSON
516
- tools_json: data.tools ? JSON.stringify(data.tools) : null,
517
- skills_json: data.skills ? JSON.stringify(data.skills) : null,
518
- // Memory
519
- memory_enabled: data.memoryEnabled || false,
520
- memory_window: data.memoryWindow || 10,
521
- // Classification
522
- namespace: data.namespace || "default",
523
- // Package management
524
- package_id: data.packageId || null,
525
- managed_by: data.managedBy || "user",
526
- // Audit
527
- created_by: data.createdBy || null,
528
- created_at: data.createdAt || now,
529
- updated_by: data.updatedBy || null,
530
- updated_at: now
531
- };
382
+ /** Resets hit/miss counters without dropping cached entries. */
383
+ resetStats() {
384
+ this.hits = 0;
385
+ this.misses = 0;
532
386
  }
533
- /**
534
- * Project tool metadata to sys_tool
535
- */
536
- projectTool(name, data, now) {
537
- return {
538
- name,
539
- project_id: this.scope.projectId ?? null,
540
- label: data.label || name,
541
- description: data.description || "",
542
- // Parameters and implementation
543
- parameters_json: data.parameters ? JSON.stringify(data.parameters) : null,
544
- handler_code: data.handler || null,
545
- // Classification
546
- namespace: data.namespace || "default",
547
- // Package management
548
- package_id: data.packageId || null,
549
- managed_by: data.managedBy || "user",
550
- // Audit
551
- created_by: data.createdBy || null,
552
- created_at: data.createdAt || now,
553
- updated_by: data.updatedBy || null,
554
- updated_at: now
555
- };
556
- }
557
- /**
558
- * Project flow metadata to sys_flow
559
- */
560
- projectFlow(name, data, now) {
561
- return {
562
- name,
563
- project_id: this.scope.projectId ?? null,
564
- label: data.label || name,
565
- description: data.description || "",
566
- flow_type: data.type || "autolaunched",
567
- // Flow definition
568
- nodes_json: data.nodes ? JSON.stringify(data.nodes) : null,
569
- edges_json: data.edges ? JSON.stringify(data.edges) : null,
570
- variables_json: data.variables ? JSON.stringify(data.variables) : null,
571
- // Trigger configuration
572
- trigger_type: data.triggerType || null,
573
- trigger_object: data.triggerObject || null,
574
- // Status
575
- active: data.active || false,
576
- // Classification
577
- namespace: data.namespace || "default",
578
- // Package management
579
- package_id: data.packageId || null,
580
- managed_by: data.managedBy || "user",
581
- // Audit
582
- created_by: data.createdBy || null,
583
- created_at: data.createdAt || now,
584
- updated_by: data.updatedBy || null,
585
- updated_at: now
586
- };
587
- }
588
- // ==========================================
589
- // Internal CRUD helpers (driver vs engine)
590
- // ==========================================
591
- async _findOne(table, query) {
592
- if (this.engine) {
593
- return this.engine.findOne(table, query);
594
- }
595
- return this.driver.findOne(table, { object: table, ...query });
596
- }
597
- async _create(table, data) {
598
- if (this.engine) {
599
- return this.engine.insert(table, data);
600
- }
601
- return this.driver.create(table, data);
602
- }
603
- async _update(table, id, data) {
604
- if (this.engine) {
605
- return this.engine.update(table, { id, ...data });
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
+ }
606
423
  }
607
- return this.driver.update(table, id, data);
608
- }
609
- async _delete(table, id) {
610
- if (this.engine) {
611
- return this.engine.delete(table, { where: { id } });
424
+ if (/already exists/i.test(msg)) {
425
+ return { index: INDEX_NAME, status: "already_exists" };
612
426
  }
613
- return this.driver.delete(table, id);
427
+ return { index: INDEX_NAME, status: "error", error: msg };
614
428
  }
615
- /**
616
- * Generate a simple unique ID
617
- */
618
- generateId() {
619
- if (typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.randomUUID === "function") {
620
- return globalThis.crypto.randomUUID();
621
- }
622
- return `proj_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
623
- }
624
- };
429
+ }
625
430
 
626
431
  // src/loaders/database-loader.ts
627
432
  var DatabaseLoader = class {
@@ -648,17 +453,56 @@ var DatabaseLoader = class {
648
453
  this.organizationId = options.organizationId;
649
454
  this.projectId = options.projectId;
650
455
  this.trackHistory = options.trackHistory !== false;
651
- this.enableProjection = options.enableProjection !== false;
652
- if (this.enableProjection) {
653
- this.projector = new MetadataProjector({
654
- driver: this.driver,
655
- engine: this.engine,
656
- organizationId: this.organizationId,
657
- projectId: this.projectId
658
- });
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);
659
467
  }
660
468
  }
661
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
+ // ==========================================
662
506
  // Internal CRUD helpers (driver vs engine)
663
507
  // ==========================================
664
508
  async _find(table, query) {
@@ -706,6 +550,23 @@ var DatabaseLoader = class {
706
550
  if (this.schemaReady) return;
707
551
  if (this.engine) {
708
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
+ }
709
570
  return;
710
571
  }
711
572
  try {
@@ -714,6 +575,10 @@ var DatabaseLoader = class {
714
575
  name: this.tableName
715
576
  });
716
577
  this.schemaReady = true;
578
+ try {
579
+ await addSysMetadataOverlayIndex(this.driver);
580
+ } catch {
581
+ }
717
582
  } catch {
718
583
  this.schemaReady = true;
719
584
  }
@@ -858,11 +723,24 @@ var DatabaseLoader = class {
858
723
  async load(type, name, _options) {
859
724
  const startTime = Date.now();
860
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
+ }
861
738
  try {
862
739
  const row = await this._findOne(this.tableName, {
863
740
  where: this.baseFilter(type, name)
864
741
  });
865
742
  if (!row) {
743
+ this.loadCache?.set(key, null);
866
744
  return {
867
745
  data: null,
868
746
  loadTime: Date.now() - startTime
@@ -870,6 +748,7 @@ var DatabaseLoader = class {
870
748
  }
871
749
  const data = this.rowToData(row);
872
750
  const record = this.rowToRecord(row);
751
+ this.loadCache?.set(key, data);
873
752
  return {
874
753
  data,
875
754
  source: "database",
@@ -886,17 +765,27 @@ var DatabaseLoader = class {
886
765
  }
887
766
  async loadMany(type, _options) {
888
767
  await this.ensureSchema();
768
+ if (this.loadManyCache) {
769
+ const cached = this.loadManyCache.get(type);
770
+ if (cached !== void 0) return cached;
771
+ }
889
772
  try {
890
773
  const rows = await this._find(this.tableName, {
891
774
  where: this.baseFilter(type)
892
775
  });
893
- return rows.map((row) => this.rowToData(row)).filter((data) => data !== null);
776
+ const result = rows.map((row) => this.rowToData(row)).filter((data) => data !== null);
777
+ this.loadManyCache?.set(type, result);
778
+ return result;
894
779
  } catch {
895
780
  return [];
896
781
  }
897
782
  }
898
783
  async exists(type, name) {
899
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
+ }
900
789
  try {
901
790
  const count = await this._count(this.tableName, {
902
791
  where: this.baseFilter(type, name)
@@ -908,31 +797,47 @@ var DatabaseLoader = class {
908
797
  }
909
798
  async stat(type, name) {
910
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
+ }
911
805
  try {
912
806
  const row = await this._findOne(this.tableName, {
913
807
  where: this.baseFilter(type, name)
914
808
  });
915
- if (!row) return null;
809
+ if (!row) {
810
+ this.statCache?.set(key, null);
811
+ return null;
812
+ }
916
813
  const record = this.rowToRecord(row);
917
814
  const metadataStr = typeof row.metadata === "string" ? row.metadata : JSON.stringify(row.metadata);
918
- return {
815
+ const stats = {
919
816
  size: metadataStr.length,
920
817
  mtime: record.updatedAt ?? record.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
921
818
  format: "json",
922
819
  etag: record.checksum
923
820
  };
821
+ this.statCache?.set(key, stats);
822
+ return stats;
924
823
  } catch {
925
824
  return null;
926
825
  }
927
826
  }
928
827
  async list(type) {
929
828
  await this.ensureSchema();
829
+ if (this.listCache) {
830
+ const cached = this.listCache.get(type);
831
+ if (cached !== void 0) return cached;
832
+ }
930
833
  try {
931
834
  const rows = await this._find(this.tableName, {
932
835
  where: this.baseFilter(type),
933
836
  fields: ["name"]
934
837
  });
935
- return rows.map((row) => row.name).filter((name) => typeof name === "string");
838
+ const names = rows.map((row) => row.name).filter((name) => typeof name === "string");
839
+ this.listCache?.set(type, names);
840
+ return names;
936
841
  } catch {
937
842
  return [];
938
843
  }
@@ -1072,6 +977,7 @@ var DatabaseLoader = class {
1072
977
  updated_at: now,
1073
978
  state: "active"
1074
979
  });
980
+ this.invalidate(type, name);
1075
981
  await this.createHistoryRecord(
1076
982
  existing.id,
1077
983
  type,
@@ -1097,6 +1003,7 @@ var DatabaseLoader = class {
1097
1003
  if (existing) {
1098
1004
  const previousChecksum = existing.checksum;
1099
1005
  if (newChecksum === previousChecksum) {
1006
+ this.loadCache?.set(this.cacheKey(type, name), data);
1100
1007
  return {
1101
1008
  success: true,
1102
1009
  path: `datasource://${this.tableName}/${type}/${name}`,
@@ -1112,6 +1019,7 @@ var DatabaseLoader = class {
1112
1019
  updated_at: now,
1113
1020
  state: "active"
1114
1021
  });
1022
+ this.invalidate(type, name);
1115
1023
  await this.createHistoryRecord(
1116
1024
  existing.id,
1117
1025
  type,
@@ -1121,9 +1029,6 @@ var DatabaseLoader = class {
1121
1029
  "update",
1122
1030
  previousChecksum
1123
1031
  );
1124
- if (this.projector) {
1125
- await this.projector.project(type, name, data);
1126
- }
1127
1032
  return {
1128
1033
  success: true,
1129
1034
  path: `datasource://${this.tableName}/${type}/${name}`,
@@ -1149,6 +1054,7 @@ var DatabaseLoader = class {
1149
1054
  created_at: now,
1150
1055
  updated_at: now
1151
1056
  });
1057
+ this.invalidate(type, name);
1152
1058
  await this.createHistoryRecord(
1153
1059
  id,
1154
1060
  type,
@@ -1157,9 +1063,6 @@ var DatabaseLoader = class {
1157
1063
  data,
1158
1064
  "create"
1159
1065
  );
1160
- if (this.projector) {
1161
- await this.projector.project(type, name, data);
1162
- }
1163
1066
  return {
1164
1067
  success: true,
1165
1068
  path: `datasource://${this.tableName}/${type}/${name}`,
@@ -1185,9 +1088,7 @@ var DatabaseLoader = class {
1185
1088
  return;
1186
1089
  }
1187
1090
  await this._delete(this.tableName, existing.id);
1188
- if (this.projector) {
1189
- await this.projector.deleteProjection(type, name);
1190
- }
1091
+ this.invalidate(type, name);
1191
1092
  }
1192
1093
  };
1193
1094
  function generateId() {
@@ -1198,7 +1099,7 @@ function generateId() {
1198
1099
  }
1199
1100
 
1200
1101
  // src/metadata-manager.ts
1201
- var MetadataManager = class {
1102
+ var _MetadataManager = class _MetadataManager {
1202
1103
  constructor(config) {
1203
1104
  this.loaders = /* @__PURE__ */ new Map();
1204
1105
  this.watchCallbacks = /* @__PURE__ */ new Map();
@@ -1210,6 +1111,18 @@ var MetadataManager = class {
1210
1111
  this.typeRegistry = [];
1211
1112
  // Dependency tracking: "type:name" -> dependencies
1212
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();
1213
1126
  this.config = config;
1214
1127
  this.logger = (0, import_core.createLogger)({ level: "info", format: "pretty" });
1215
1128
  this.serializers = /* @__PURE__ */ new Map();
@@ -1260,7 +1173,8 @@ var MetadataManager = class {
1260
1173
  driver,
1261
1174
  tableName,
1262
1175
  organizationId,
1263
- projectId
1176
+ projectId,
1177
+ cache: this.config.cache?.databaseLoader
1264
1178
  });
1265
1179
  this.registerLoader(dbLoader);
1266
1180
  this.logger.info("DatabaseLoader configured", { datasource: this.config.datasource, tableName });
@@ -1288,7 +1202,8 @@ var MetadataManager = class {
1288
1202
  engine,
1289
1203
  tableName,
1290
1204
  organizationId,
1291
- projectId
1205
+ projectId,
1206
+ cache: this.config.cache?.databaseLoader
1292
1207
  });
1293
1208
  this.registerLoader(dbLoader);
1294
1209
  this.logger.info("DatabaseLoader configured via DataEngine", { tableName });
@@ -1320,10 +1235,19 @@ var MetadataManager = class {
1320
1235
  * should not be written to during runtime registration.
1321
1236
  */
1322
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
+ }
1323
1246
  if (!this.registry.has(type)) {
1324
1247
  this.registry.set(type, /* @__PURE__ */ new Map());
1325
1248
  }
1326
1249
  this.registry.get(type).set(name, data);
1250
+ this.invalidateListCache(type);
1327
1251
  for (const loader of this.loaders.values()) {
1328
1252
  if (loader.save && loader.contract.protocol === "datasource:" && loader.contract.capabilities.write) {
1329
1253
  await loader.save(type, name, data);
@@ -1365,6 +1289,10 @@ var MetadataManager = class {
1365
1289
  * List all metadata items of a given type
1366
1290
  */
1367
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
+ }
1368
1296
  const items = /* @__PURE__ */ new Map();
1369
1297
  const typeStore = this.registry.get(type);
1370
1298
  if (typeStore) {
@@ -1385,7 +1313,16 @@ var MetadataManager = class {
1385
1313
  this.logger.warn(`Loader ${loader.contract.name} failed to loadMany ${type}`, { error: e });
1386
1314
  }
1387
1315
  }
1388
- return Array.from(items.values());
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);
1389
1326
  }
1390
1327
  /**
1391
1328
  * Unregister/remove a metadata item by type and name.
@@ -1399,6 +1336,7 @@ var MetadataManager = class {
1399
1336
  this.registry.delete(type);
1400
1337
  }
1401
1338
  }
1339
+ this.invalidateListCache(type);
1402
1340
  for (const loader of this.loaders.values()) {
1403
1341
  if (loader.contract.protocol !== "datasource:" || !loader.contract.capabilities.write) continue;
1404
1342
  if (typeof loader.delete === "function") {
@@ -1813,6 +1751,14 @@ var MetadataManager = class {
1813
1751
  * Save/update an overlay for a metadata item
1814
1752
  */
1815
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
+ }
1816
1762
  const key = this.overlayKey(overlay.baseType, overlay.baseName, overlay.scope);
1817
1763
  this.overlays.set(key, overlay);
1818
1764
  }
@@ -2282,6 +2228,8 @@ var MetadataManager = class {
2282
2228
  };
2283
2229
  }
2284
2230
  };
2231
+ _MetadataManager.LIST_CACHE_TTL_MS = 3e4;
2232
+ var MetadataManager = _MetadataManager;
2285
2233
 
2286
2234
  // src/plugin.ts
2287
2235
  var import_promises = require("fs/promises");
@@ -2773,7 +2721,10 @@ var MemoryLoader = class {
2773
2721
  // src/plugin.ts
2774
2722
  var import_kernel = require("@objectstack/spec/kernel");
2775
2723
  var import_metadata2 = require("@objectstack/platform-objects/metadata");
2776
- var queryableMetadataObjects = [import_metadata2.SysObject, import_metadata2.SysView, import_metadata2.SysFlow, import_metadata2.SysAgent, import_metadata2.SysTool];
2724
+ var queryableMetadataObjects = [
2725
+ import_metadata2.SysMetadataObject,
2726
+ import_metadata2.SysMetadataHistoryObject
2727
+ ];
2777
2728
  var ARTIFACT_FIELD_TO_TYPE = {
2778
2729
  objects: "object",
2779
2730
  objectExtensions: "object_extension",
@@ -2841,13 +2792,35 @@ var MetadataPlugin = class {
2841
2792
  };
2842
2793
  this.start = async (ctx) => {
2843
2794
  const src = this.options.artifactSource;
2844
- if (src?.mode === "local-file") {
2845
- await this._loadFromLocalFile(ctx, src.path);
2846
- } else if (src?.mode === "artifact-api") {
2847
- ctx.logger.warn("[MetadataPlugin] artifact-api source is not yet implemented; falling back to file-system scan");
2848
- await this._loadFromFileSystem(ctx);
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
+ }
2849
2816
  } else {
2850
- await this._loadFromFileSystem(ctx);
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
+ }
2851
2824
  }
2852
2825
  try {
2853
2826
  const realtimeService = ctx.getService("realtime");
@@ -2866,45 +2839,46 @@ var MetadataPlugin = class {
2866
2839
  ...options
2867
2840
  };
2868
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;
2869
2844
  this.manager = new NodeMetadataManager({
2870
2845
  rootDir,
2871
- watch: this.options.watch ?? true,
2846
+ watch: effectiveWatch,
2872
2847
  formats: ["yaml", "json", "typescript", "javascript"]
2873
2848
  });
2874
2849
  this.manager.setTypeRegistry(import_kernel.DEFAULT_METADATA_TYPE_REGISTRY);
2875
2850
  }
2876
- async _loadFromLocalFile(ctx, filePath) {
2877
- const isUrl = /^https?:\/\//i.test(filePath);
2878
- ctx.logger.info(
2879
- `[MetadataPlugin] Loading metadata from ${isUrl ? "remote URL" : "local artifact file"}`,
2880
- { path: filePath }
2881
- );
2882
- let raw;
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;
2883
2859
  try {
2884
- let content;
2885
- if (isUrl) {
2886
- const controller = new AbortController();
2887
- const timer = setTimeout(() => controller.abort(), 15e3);
2888
- try {
2889
- const res = await fetch(filePath, {
2890
- redirect: "follow",
2891
- signal: controller.signal,
2892
- headers: { Accept: "application/json, */*;q=0.5" }
2893
- });
2894
- if (!res.ok) {
2895
- throw new Error(`HTTP ${res.status} ${res.statusText}`);
2896
- }
2897
- content = await res.text();
2898
- } finally {
2899
- clearTimeout(timer);
2900
- }
2901
- } else {
2902
- content = await (0, import_promises.readFile)(filePath, "utf8");
2903
- }
2904
- raw = JSON.parse(content);
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);
2905
2866
  } catch (e) {
2906
- throw new Error(`[MetadataPlugin] Cannot read artifact ${isUrl ? "URL" : "file"} at "${filePath}": ${e.message}`);
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);
2907
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) {
2908
2882
  const { ProjectArtifactSchema } = await import("@objectstack/spec/cloud");
2909
2883
  const { ObjectStackDefinitionSchema } = await import("@objectstack/spec");
2910
2884
  let metadata;
@@ -2912,6 +2886,9 @@ var MetadataPlugin = class {
2912
2886
  if (obj?.schemaVersion && obj?.commitId && obj?.metadata !== void 0) {
2913
2887
  const artifact = ProjectArtifactSchema.parse(obj);
2914
2888
  metadata = artifact.metadata;
2889
+ } else if (obj?.success && obj?.data?.metadata) {
2890
+ const artifact = ProjectArtifactSchema.parse(obj.data);
2891
+ metadata = artifact.metadata;
2915
2892
  } else {
2916
2893
  const def = ObjectStackDefinitionSchema.parse(obj);
2917
2894
  const canonical = JSON.stringify(def, Object.keys(def).sort());
@@ -2944,10 +2921,51 @@ var MetadataPlugin = class {
2944
2921
  }
2945
2922
  }
2946
2923
  this.manager.registerLoader(memLoader);
2947
- ctx.logger.info("[MetadataPlugin] Artifact metadata loaded", {
2948
- path: filePath,
2949
- totalRegistered
2950
- });
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);
2951
2969
  }
2952
2970
  async _loadFromFileSystem(ctx) {
2953
2971
  ctx.logger.info("Loading metadata from file system...");
@@ -3470,7 +3488,6 @@ var MigrationExecutor = class {
3470
3488
  MemoryLoader,
3471
3489
  MetadataManager,
3472
3490
  MetadataPlugin,
3473
- MetadataProjector,
3474
3491
  Migration,
3475
3492
  RemoteLoader,
3476
3493
  SysMetadataHistoryObject,