@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.js CHANGED
@@ -275,307 +275,113 @@ function generateDiffSummary(diff) {
275
275
  return summary.join(", ");
276
276
  }
277
277
 
278
- // src/projection/metadata-projector.ts
279
- import { StorageNameMapping } from "@objectstack/spec/system";
280
- var MetadataProjector = class {
281
- constructor(options) {
282
- // Map of metadata types to their target table names
283
- this.typeTableMap = {
284
- object: "sys_object",
285
- view: "sys_view",
286
- agent: "sys_agent",
287
- tool: "sys_tool",
288
- flow: "sys_flow"
289
- // Add more as needed: dashboard, app, action, workflow, etc.
290
- };
291
- if (!options.driver && !options.engine) {
292
- throw new Error("MetadataProjector requires either a driver or engine");
293
- }
294
- this.driver = options.driver;
295
- this.engine = options.engine;
296
- this.scope = {
297
- organizationId: options.organizationId,
298
- projectId: options.projectId
299
- };
278
+ // src/utils/lru-cache.ts
279
+ var LRUCache = class {
280
+ constructor(options = {}) {
281
+ this.map = /* @__PURE__ */ new Map();
282
+ this.hits = 0;
283
+ this.misses = 0;
284
+ this.maxSize = options.maxSize && options.maxSize > 0 ? options.maxSize : 0;
285
+ this.ttl = options.ttl && options.ttl > 0 ? options.ttl : 0;
286
+ }
287
+ get(key) {
288
+ const entry = this.map.get(key);
289
+ if (!entry) {
290
+ this.misses++;
291
+ return void 0;
292
+ }
293
+ if (entry.expiresAt !== 0 && entry.expiresAt <= Date.now()) {
294
+ this.map.delete(key);
295
+ this.misses++;
296
+ return void 0;
297
+ }
298
+ this.map.delete(key);
299
+ this.map.set(key, entry);
300
+ this.hits++;
301
+ return entry.value;
302
+ }
303
+ set(key, value) {
304
+ if (this.map.has(key)) {
305
+ this.map.delete(key);
306
+ } else if (this.maxSize > 0 && this.map.size >= this.maxSize) {
307
+ const oldest = this.map.keys().next();
308
+ if (!oldest.done) this.map.delete(oldest.value);
309
+ }
310
+ this.map.set(key, {
311
+ value,
312
+ expiresAt: this.ttl > 0 ? Date.now() + this.ttl : 0
313
+ });
300
314
  }
301
- /**
302
- * Project metadata to type-specific table
303
- */
304
- async project(type, name, data) {
305
- const targetTable = this.typeTableMap[type];
306
- if (!targetTable) {
307
- return;
308
- }
309
- const projectedData = this.transformToProjection(type, name, data);
310
- if (!projectedData) {
311
- return;
312
- }
313
- try {
314
- const projId = this.scope.projectId ?? null;
315
- const existing = await this._findOne(targetTable, {
316
- where: { name, project_id: projId }
317
- });
318
- if (existing) {
319
- await this._update(targetTable, existing.id, projectedData);
320
- } else {
321
- const id = this.generateId();
322
- await this._create(targetTable, {
323
- id,
324
- ...projectedData
325
- });
326
- }
327
- } catch (error) {
328
- console.error(`Failed to project ${type}/${name} to ${targetTable}:`, error);
329
- }
315
+ has(key) {
316
+ return this.get(key) !== void 0;
330
317
  }
331
- /**
332
- * Delete projection from type-specific table
333
- */
334
- async deleteProjection(type, name) {
335
- const targetTable = this.typeTableMap[type];
336
- if (!targetTable) {
337
- return;
338
- }
339
- try {
340
- const projId = this.scope.projectId ?? null;
341
- const existing = await this._findOne(targetTable, {
342
- where: { name, project_id: projId }
343
- });
344
- if (existing) {
345
- await this._delete(targetTable, existing.id);
346
- }
347
- } catch (error) {
348
- console.error(`Failed to delete projection ${type}/${name} from ${targetTable}:`, error);
349
- }
318
+ delete(key) {
319
+ return this.map.delete(key);
350
320
  }
351
- /**
352
- * Transform metadata into projection record
353
- */
354
- transformToProjection(type, name, data) {
355
- const now = (/* @__PURE__ */ new Date()).toISOString();
356
- switch (type) {
357
- case "object":
358
- return this.projectObject(name, data, now);
359
- case "view":
360
- return this.projectView(name, data, now);
361
- case "agent":
362
- return this.projectAgent(name, data, now);
363
- case "tool":
364
- return this.projectTool(name, data, now);
365
- case "flow":
366
- return this.projectFlow(name, data, now);
367
- default:
368
- return null;
369
- }
321
+ clear() {
322
+ this.map.clear();
370
323
  }
371
- /**
372
- * Project object metadata to sys_object
373
- */
374
- projectObject(name, data, now) {
375
- return {
376
- name,
377
- project_id: this.scope.projectId ?? null,
378
- label: data.label || name,
379
- plural_label: data.pluralLabel || data.label || name,
380
- description: data.description || "",
381
- icon: data.icon || "database",
382
- namespace: data.namespace || "default",
383
- tags: Array.isArray(data.tags) ? data.tags.join(",") : data.tags || "",
384
- active: data.active !== false,
385
- is_system: data.isSystem || false,
386
- abstract: data.abstract || false,
387
- datasource: data.datasource || "default",
388
- table_name: data.name ? StorageNameMapping.resolveTableName({ name: data.name }) : name,
389
- // Serialize complex structures as JSON
390
- fields_json: data.fields ? JSON.stringify(data.fields) : null,
391
- indexes_json: data.indexes ? JSON.stringify(data.indexes) : null,
392
- validations_json: data.validations ? JSON.stringify(data.validations) : null,
393
- state_machines_json: data.stateMachines ? JSON.stringify(data.stateMachines) : null,
394
- capabilities_json: data.enable ? JSON.stringify(data.enable) : null,
395
- // Denormalized fields
396
- field_count: data.fields ? Object.keys(data.fields).length : 0,
397
- display_name_field: data.displayNameField || null,
398
- title_format: data.titleFormat || null,
399
- compact_layout: Array.isArray(data.compactLayout) ? data.compactLayout.join(",") : data.compactLayout || null,
400
- // Capabilities (denormalized for easier querying)
401
- track_history: data.enable?.trackHistory || false,
402
- searchable: data.enable?.searchable !== false,
403
- api_enabled: data.enable?.apiEnabled !== false,
404
- files: data.enable?.files || false,
405
- feeds: data.enable?.feeds || false,
406
- activities: data.enable?.activities || false,
407
- trash: data.enable?.trash !== false,
408
- mru: data.enable?.mru !== false,
409
- clone: data.enable?.clone !== false,
410
- // Package management
411
- package_id: data.packageId || null,
412
- managed_by: data.managedBy || "user",
413
- // Audit
414
- created_by: data.createdBy || null,
415
- created_at: data.createdAt || now,
416
- updated_by: data.updatedBy || null,
417
- updated_at: now
418
- };
324
+ get size() {
325
+ return this.map.size;
419
326
  }
420
- /**
421
- * Project view metadata to sys_view
422
- */
423
- projectView(name, data, now) {
327
+ /** Diagnostic counters — useful for `metrics` endpoints. */
328
+ stats() {
329
+ const total = this.hits + this.misses;
424
330
  return {
425
- name,
426
- project_id: this.scope.projectId ?? null,
427
- label: data.label || name,
428
- description: data.description || "",
429
- object_name: data.object || "",
430
- view_type: data.type || "grid",
431
- // Serialize configurations as JSON
432
- columns_json: data.columns ? JSON.stringify(data.columns) : null,
433
- filters_json: data.filters ? JSON.stringify(data.filters) : null,
434
- sort_json: data.sort ? JSON.stringify(data.sort) : null,
435
- config_json: data.config ? JSON.stringify(data.config) : null,
436
- // Display options
437
- page_size: data.pageSize || 25,
438
- show_search: data.showSearch !== false,
439
- show_filters: data.showFilters !== false,
440
- // Classification
441
- namespace: data.namespace || "default",
442
- // Package management
443
- package_id: data.packageId || null,
444
- managed_by: data.managedBy || "user",
445
- // Audit
446
- created_by: data.createdBy || null,
447
- created_at: data.createdAt || now,
448
- updated_by: data.updatedBy || null,
449
- updated_at: now
331
+ size: this.map.size,
332
+ hits: this.hits,
333
+ misses: this.misses,
334
+ hitRate: total === 0 ? 0 : this.hits / total
450
335
  };
451
336
  }
452
- /**
453
- * Project agent metadata to sys_agent
454
- */
455
- projectAgent(name, data, now) {
456
- return {
457
- name,
458
- project_id: this.scope.projectId ?? null,
459
- label: data.label || name,
460
- description: data.description || "",
461
- agent_type: data.type || "conversational",
462
- // Model configuration
463
- model: data.model || null,
464
- temperature: data.temperature ?? 0.7,
465
- max_tokens: data.maxTokens || null,
466
- top_p: data.topP || null,
467
- // System prompt
468
- system_prompt: data.systemPrompt || null,
469
- // Tools and skills as JSON
470
- tools_json: data.tools ? JSON.stringify(data.tools) : null,
471
- skills_json: data.skills ? JSON.stringify(data.skills) : null,
472
- // Memory
473
- memory_enabled: data.memoryEnabled || false,
474
- memory_window: data.memoryWindow || 10,
475
- // Classification
476
- namespace: data.namespace || "default",
477
- // Package management
478
- package_id: data.packageId || null,
479
- managed_by: data.managedBy || "user",
480
- // Audit
481
- created_by: data.createdBy || null,
482
- created_at: data.createdAt || now,
483
- updated_by: data.updatedBy || null,
484
- updated_at: now
485
- };
337
+ /** Resets hit/miss counters without dropping cached entries. */
338
+ resetStats() {
339
+ this.hits = 0;
340
+ this.misses = 0;
486
341
  }
487
- /**
488
- * Project tool metadata to sys_tool
489
- */
490
- projectTool(name, data, now) {
491
- return {
492
- name,
493
- project_id: this.scope.projectId ?? null,
494
- label: data.label || name,
495
- description: data.description || "",
496
- // Parameters and implementation
497
- parameters_json: data.parameters ? JSON.stringify(data.parameters) : null,
498
- handler_code: data.handler || null,
499
- // Classification
500
- namespace: data.namespace || "default",
501
- // Package management
502
- package_id: data.packageId || null,
503
- managed_by: data.managedBy || "user",
504
- // Audit
505
- created_by: data.createdBy || null,
506
- created_at: data.createdAt || now,
507
- updated_by: data.updatedBy || null,
508
- updated_at: now
509
- };
510
- }
511
- /**
512
- * Project flow metadata to sys_flow
513
- */
514
- projectFlow(name, data, now) {
515
- return {
516
- name,
517
- project_id: this.scope.projectId ?? null,
518
- label: data.label || name,
519
- description: data.description || "",
520
- flow_type: data.type || "autolaunched",
521
- // Flow definition
522
- nodes_json: data.nodes ? JSON.stringify(data.nodes) : null,
523
- edges_json: data.edges ? JSON.stringify(data.edges) : null,
524
- variables_json: data.variables ? JSON.stringify(data.variables) : null,
525
- // Trigger configuration
526
- trigger_type: data.triggerType || null,
527
- trigger_object: data.triggerObject || null,
528
- // Status
529
- active: data.active || false,
530
- // Classification
531
- namespace: data.namespace || "default",
532
- // Package management
533
- package_id: data.packageId || null,
534
- managed_by: data.managedBy || "user",
535
- // Audit
536
- created_by: data.createdBy || null,
537
- created_at: data.createdAt || now,
538
- updated_by: data.updatedBy || null,
539
- updated_at: now
540
- };
541
- }
542
- // ==========================================
543
- // Internal CRUD helpers (driver vs engine)
544
- // ==========================================
545
- async _findOne(table, query) {
546
- if (this.engine) {
547
- return this.engine.findOne(table, query);
548
- }
549
- return this.driver.findOne(table, { object: table, ...query });
550
- }
551
- async _create(table, data) {
552
- if (this.engine) {
553
- return this.engine.insert(table, data);
554
- }
555
- return this.driver.create(table, data);
556
- }
557
- async _update(table, id, data) {
558
- if (this.engine) {
559
- return this.engine.update(table, { id, ...data });
342
+ };
343
+
344
+ // src/migrations/add-sys-metadata-overlay-index.ts
345
+ var INDEX_NAME = "idx_sys_metadata_overlay_active";
346
+ var TABLE = "sys_metadata";
347
+ var COLUMNS = "(type, name, organization_id, project_id, scope)";
348
+ var WHERE = "state = 'active'";
349
+ async function addSysMetadataOverlayIndex(driver) {
350
+ const driverAny = driver;
351
+ const exec = async (sql) => {
352
+ if (typeof driverAny.raw === "function") {
353
+ await driverAny.raw(sql);
354
+ } else if (typeof driverAny.execute === "function") {
355
+ await driverAny.execute(sql);
356
+ } else {
357
+ throw new Error("driver has neither raw nor execute");
358
+ }
359
+ };
360
+ const partialSql = `CREATE UNIQUE INDEX IF NOT EXISTS ${INDEX_NAME} ON ${TABLE} ${COLUMNS} WHERE ${WHERE}`;
361
+ const fallbackSql = `CREATE INDEX IF NOT EXISTS ${INDEX_NAME} ON ${TABLE} ${COLUMNS}`;
362
+ try {
363
+ await exec(partialSql);
364
+ return { index: INDEX_NAME, status: "created" };
365
+ } catch (err) {
366
+ const msg = err instanceof Error ? err.message : String(err);
367
+ if (/partial|where clause|syntax/i.test(msg)) {
368
+ try {
369
+ await exec(fallbackSql);
370
+ return { index: INDEX_NAME, status: "fallback_non_unique" };
371
+ } catch (fallbackErr) {
372
+ return {
373
+ index: INDEX_NAME,
374
+ status: "error",
375
+ error: fallbackErr instanceof Error ? fallbackErr.message : String(fallbackErr)
376
+ };
377
+ }
560
378
  }
561
- return this.driver.update(table, id, data);
562
- }
563
- async _delete(table, id) {
564
- if (this.engine) {
565
- return this.engine.delete(table, { where: { id } });
379
+ if (/already exists/i.test(msg)) {
380
+ return { index: INDEX_NAME, status: "already_exists" };
566
381
  }
567
- return this.driver.delete(table, id);
382
+ return { index: INDEX_NAME, status: "error", error: msg };
568
383
  }
569
- /**
570
- * Generate a simple unique ID
571
- */
572
- generateId() {
573
- if (typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.randomUUID === "function") {
574
- return globalThis.crypto.randomUUID();
575
- }
576
- return `proj_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
577
- }
578
- };
384
+ }
579
385
 
580
386
  // src/loaders/database-loader.ts
581
387
  var DatabaseLoader = class {
@@ -602,17 +408,56 @@ var DatabaseLoader = class {
602
408
  this.organizationId = options.organizationId;
603
409
  this.projectId = options.projectId;
604
410
  this.trackHistory = options.trackHistory !== false;
605
- this.enableProjection = options.enableProjection !== false;
606
- if (this.enableProjection) {
607
- this.projector = new MetadataProjector({
608
- driver: this.driver,
609
- engine: this.engine,
610
- organizationId: this.organizationId,
611
- projectId: this.projectId
612
- });
411
+ const cacheOpts = options.cache;
412
+ const cacheEnabled = cacheOpts?.enabled !== false;
413
+ if (cacheEnabled) {
414
+ const lruOpts = {
415
+ maxSize: cacheOpts?.maxSize ?? 500,
416
+ ttl: cacheOpts?.ttl ?? 6e4
417
+ };
418
+ this.loadCache = new LRUCache(lruOpts);
419
+ this.loadManyCache = new LRUCache(lruOpts);
420
+ this.listCache = new LRUCache(lruOpts);
421
+ this.statCache = new LRUCache(lruOpts);
613
422
  }
614
423
  }
615
424
  // ==========================================
425
+ // Cache helpers
426
+ // ==========================================
427
+ cacheKey(type, name) {
428
+ return `${type}::${name}`;
429
+ }
430
+ /**
431
+ * Invalidate all cached entries for a specific (type, name) pair plus
432
+ * the type-level aggregates (`loadMany`, `list`). Called from every write
433
+ * path (`save`, `delete`, `registerRollback`).
434
+ */
435
+ invalidate(type, name) {
436
+ if (!this.loadCache) return;
437
+ const key = this.cacheKey(type, name);
438
+ this.loadCache.delete(key);
439
+ this.statCache?.delete(key);
440
+ this.loadManyCache?.delete(type);
441
+ this.listCache?.delete(type);
442
+ }
443
+ /** Drop the entire cache — useful after bulk imports or schema changes. */
444
+ invalidateAll() {
445
+ this.loadCache?.clear();
446
+ this.loadManyCache?.clear();
447
+ this.listCache?.clear();
448
+ this.statCache?.clear();
449
+ }
450
+ /** Diagnostic: aggregated cache statistics for `metrics` endpoints. */
451
+ getCacheStats() {
452
+ return {
453
+ enabled: this.loadCache !== void 0,
454
+ load: this.loadCache?.stats() ?? null,
455
+ loadMany: this.loadManyCache?.stats() ?? null,
456
+ list: this.listCache?.stats() ?? null,
457
+ stat: this.statCache?.stats() ?? null
458
+ };
459
+ }
460
+ // ==========================================
616
461
  // Internal CRUD helpers (driver vs engine)
617
462
  // ==========================================
618
463
  async _find(table, query) {
@@ -660,6 +505,23 @@ var DatabaseLoader = class {
660
505
  if (this.schemaReady) return;
661
506
  if (this.engine) {
662
507
  this.schemaReady = true;
508
+ try {
509
+ const engineAny = this.engine;
510
+ let driver = engineAny?.driver ?? engineAny?.getDriver?.();
511
+ if (!driver && engineAny?.drivers instanceof Map) {
512
+ for (const candidate of engineAny.drivers.values()) {
513
+ const c = candidate;
514
+ if (c && (typeof c.raw === "function" || typeof c.execute === "function")) {
515
+ driver = candidate;
516
+ break;
517
+ }
518
+ }
519
+ }
520
+ if (driver) {
521
+ await addSysMetadataOverlayIndex(driver);
522
+ }
523
+ } catch {
524
+ }
663
525
  return;
664
526
  }
665
527
  try {
@@ -668,6 +530,10 @@ var DatabaseLoader = class {
668
530
  name: this.tableName
669
531
  });
670
532
  this.schemaReady = true;
533
+ try {
534
+ await addSysMetadataOverlayIndex(this.driver);
535
+ } catch {
536
+ }
671
537
  } catch {
672
538
  this.schemaReady = true;
673
539
  }
@@ -812,11 +678,24 @@ var DatabaseLoader = class {
812
678
  async load(type, name, _options) {
813
679
  const startTime = Date.now();
814
680
  await this.ensureSchema();
681
+ const key = this.cacheKey(type, name);
682
+ if (this.loadCache) {
683
+ const cached = this.loadCache.get(key);
684
+ if (cached !== void 0) {
685
+ return {
686
+ data: cached,
687
+ source: "database",
688
+ format: "json",
689
+ loadTime: Date.now() - startTime
690
+ };
691
+ }
692
+ }
815
693
  try {
816
694
  const row = await this._findOne(this.tableName, {
817
695
  where: this.baseFilter(type, name)
818
696
  });
819
697
  if (!row) {
698
+ this.loadCache?.set(key, null);
820
699
  return {
821
700
  data: null,
822
701
  loadTime: Date.now() - startTime
@@ -824,6 +703,7 @@ var DatabaseLoader = class {
824
703
  }
825
704
  const data = this.rowToData(row);
826
705
  const record = this.rowToRecord(row);
706
+ this.loadCache?.set(key, data);
827
707
  return {
828
708
  data,
829
709
  source: "database",
@@ -840,17 +720,27 @@ var DatabaseLoader = class {
840
720
  }
841
721
  async loadMany(type, _options) {
842
722
  await this.ensureSchema();
723
+ if (this.loadManyCache) {
724
+ const cached = this.loadManyCache.get(type);
725
+ if (cached !== void 0) return cached;
726
+ }
843
727
  try {
844
728
  const rows = await this._find(this.tableName, {
845
729
  where: this.baseFilter(type)
846
730
  });
847
- return rows.map((row) => this.rowToData(row)).filter((data) => data !== null);
731
+ const result = rows.map((row) => this.rowToData(row)).filter((data) => data !== null);
732
+ this.loadManyCache?.set(type, result);
733
+ return result;
848
734
  } catch {
849
735
  return [];
850
736
  }
851
737
  }
852
738
  async exists(type, name) {
853
739
  await this.ensureSchema();
740
+ if (this.loadCache) {
741
+ const cached = this.loadCache.get(this.cacheKey(type, name));
742
+ if (cached !== void 0) return cached !== null;
743
+ }
854
744
  try {
855
745
  const count = await this._count(this.tableName, {
856
746
  where: this.baseFilter(type, name)
@@ -862,31 +752,47 @@ var DatabaseLoader = class {
862
752
  }
863
753
  async stat(type, name) {
864
754
  await this.ensureSchema();
755
+ const key = this.cacheKey(type, name);
756
+ if (this.statCache) {
757
+ const cached = this.statCache.get(key);
758
+ if (cached !== void 0) return cached;
759
+ }
865
760
  try {
866
761
  const row = await this._findOne(this.tableName, {
867
762
  where: this.baseFilter(type, name)
868
763
  });
869
- if (!row) return null;
764
+ if (!row) {
765
+ this.statCache?.set(key, null);
766
+ return null;
767
+ }
870
768
  const record = this.rowToRecord(row);
871
769
  const metadataStr = typeof row.metadata === "string" ? row.metadata : JSON.stringify(row.metadata);
872
- return {
770
+ const stats = {
873
771
  size: metadataStr.length,
874
772
  mtime: record.updatedAt ?? record.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
875
773
  format: "json",
876
774
  etag: record.checksum
877
775
  };
776
+ this.statCache?.set(key, stats);
777
+ return stats;
878
778
  } catch {
879
779
  return null;
880
780
  }
881
781
  }
882
782
  async list(type) {
883
783
  await this.ensureSchema();
784
+ if (this.listCache) {
785
+ const cached = this.listCache.get(type);
786
+ if (cached !== void 0) return cached;
787
+ }
884
788
  try {
885
789
  const rows = await this._find(this.tableName, {
886
790
  where: this.baseFilter(type),
887
791
  fields: ["name"]
888
792
  });
889
- return rows.map((row) => row.name).filter((name) => typeof name === "string");
793
+ const names = rows.map((row) => row.name).filter((name) => typeof name === "string");
794
+ this.listCache?.set(type, names);
795
+ return names;
890
796
  } catch {
891
797
  return [];
892
798
  }
@@ -1026,6 +932,7 @@ var DatabaseLoader = class {
1026
932
  updated_at: now,
1027
933
  state: "active"
1028
934
  });
935
+ this.invalidate(type, name);
1029
936
  await this.createHistoryRecord(
1030
937
  existing.id,
1031
938
  type,
@@ -1051,6 +958,7 @@ var DatabaseLoader = class {
1051
958
  if (existing) {
1052
959
  const previousChecksum = existing.checksum;
1053
960
  if (newChecksum === previousChecksum) {
961
+ this.loadCache?.set(this.cacheKey(type, name), data);
1054
962
  return {
1055
963
  success: true,
1056
964
  path: `datasource://${this.tableName}/${type}/${name}`,
@@ -1066,6 +974,7 @@ var DatabaseLoader = class {
1066
974
  updated_at: now,
1067
975
  state: "active"
1068
976
  });
977
+ this.invalidate(type, name);
1069
978
  await this.createHistoryRecord(
1070
979
  existing.id,
1071
980
  type,
@@ -1075,9 +984,6 @@ var DatabaseLoader = class {
1075
984
  "update",
1076
985
  previousChecksum
1077
986
  );
1078
- if (this.projector) {
1079
- await this.projector.project(type, name, data);
1080
- }
1081
987
  return {
1082
988
  success: true,
1083
989
  path: `datasource://${this.tableName}/${type}/${name}`,
@@ -1103,6 +1009,7 @@ var DatabaseLoader = class {
1103
1009
  created_at: now,
1104
1010
  updated_at: now
1105
1011
  });
1012
+ this.invalidate(type, name);
1106
1013
  await this.createHistoryRecord(
1107
1014
  id,
1108
1015
  type,
@@ -1111,9 +1018,6 @@ var DatabaseLoader = class {
1111
1018
  data,
1112
1019
  "create"
1113
1020
  );
1114
- if (this.projector) {
1115
- await this.projector.project(type, name, data);
1116
- }
1117
1021
  return {
1118
1022
  success: true,
1119
1023
  path: `datasource://${this.tableName}/${type}/${name}`,
@@ -1139,9 +1043,7 @@ var DatabaseLoader = class {
1139
1043
  return;
1140
1044
  }
1141
1045
  await this._delete(this.tableName, existing.id);
1142
- if (this.projector) {
1143
- await this.projector.deleteProjection(type, name);
1144
- }
1046
+ this.invalidate(type, name);
1145
1047
  }
1146
1048
  };
1147
1049
  function generateId() {
@@ -1152,7 +1054,7 @@ function generateId() {
1152
1054
  }
1153
1055
 
1154
1056
  // src/metadata-manager.ts
1155
- var MetadataManager = class {
1057
+ var _MetadataManager = class _MetadataManager {
1156
1058
  constructor(config) {
1157
1059
  this.loaders = /* @__PURE__ */ new Map();
1158
1060
  this.watchCallbacks = /* @__PURE__ */ new Map();
@@ -1164,6 +1066,18 @@ var MetadataManager = class {
1164
1066
  this.typeRegistry = [];
1165
1067
  // Dependency tracking: "type:name" -> dependencies
1166
1068
  this.dependencies = /* @__PURE__ */ new Map();
1069
+ // Short-lived cache for list() results. Built primarily to break the
1070
+ // deadlock that occurs when security/permission middleware calls
1071
+ // `list('permission')` from inside a user-initiated DB transaction: the
1072
+ // DatabaseLoader's `engine.find('sys_metadata', ...)` would then try to
1073
+ // acquire a fresh knex connection while the transaction is still holding
1074
+ // SQLite's single connection — knex waits the full `acquireConnectionTimeout`
1075
+ // (60s) before returning []. The cache absorbs the repeated lookups so the
1076
+ // loader is only hit once per TTL window.
1077
+ //
1078
+ // Invalidated on every `register()` / `unregister()` to keep CRUD writes
1079
+ // visible to subsequent reads.
1080
+ this.listCache = /* @__PURE__ */ new Map();
1167
1081
  this.config = config;
1168
1082
  this.logger = createLogger({ level: "info", format: "pretty" });
1169
1083
  this.serializers = /* @__PURE__ */ new Map();
@@ -1214,7 +1128,8 @@ var MetadataManager = class {
1214
1128
  driver,
1215
1129
  tableName,
1216
1130
  organizationId,
1217
- projectId
1131
+ projectId,
1132
+ cache: this.config.cache?.databaseLoader
1218
1133
  });
1219
1134
  this.registerLoader(dbLoader);
1220
1135
  this.logger.info("DatabaseLoader configured", { datasource: this.config.datasource, tableName });
@@ -1242,7 +1157,8 @@ var MetadataManager = class {
1242
1157
  engine,
1243
1158
  tableName,
1244
1159
  organizationId,
1245
- projectId
1160
+ projectId,
1161
+ cache: this.config.cache?.databaseLoader
1246
1162
  });
1247
1163
  this.registerLoader(dbLoader);
1248
1164
  this.logger.info("DatabaseLoader configured via DataEngine", { tableName });
@@ -1274,10 +1190,19 @@ var MetadataManager = class {
1274
1190
  * should not be written to during runtime registration.
1275
1191
  */
1276
1192
  async register(type, name, data) {
1193
+ if (this.config.persistence?.writable === false) {
1194
+ const msg = `MetadataManager is read-only (persistence.writable=false); refusing to register ${type}/${name}`;
1195
+ if (this.config.validation?.throwOnError) {
1196
+ throw new Error(msg);
1197
+ }
1198
+ this.logger.warn(msg);
1199
+ return;
1200
+ }
1277
1201
  if (!this.registry.has(type)) {
1278
1202
  this.registry.set(type, /* @__PURE__ */ new Map());
1279
1203
  }
1280
1204
  this.registry.get(type).set(name, data);
1205
+ this.invalidateListCache(type);
1281
1206
  for (const loader of this.loaders.values()) {
1282
1207
  if (loader.save && loader.contract.protocol === "datasource:" && loader.contract.capabilities.write) {
1283
1208
  await loader.save(type, name, data);
@@ -1319,6 +1244,10 @@ var MetadataManager = class {
1319
1244
  * List all metadata items of a given type
1320
1245
  */
1321
1246
  async list(type) {
1247
+ const cached = this.listCache.get(type);
1248
+ if (cached && Date.now() - cached.ts < _MetadataManager.LIST_CACHE_TTL_MS) {
1249
+ return cached.items;
1250
+ }
1322
1251
  const items = /* @__PURE__ */ new Map();
1323
1252
  const typeStore = this.registry.get(type);
1324
1253
  if (typeStore) {
@@ -1339,7 +1268,16 @@ var MetadataManager = class {
1339
1268
  this.logger.warn(`Loader ${loader.contract.name} failed to loadMany ${type}`, { error: e });
1340
1269
  }
1341
1270
  }
1342
- return Array.from(items.values());
1271
+ const result = Array.from(items.values());
1272
+ this.cacheListResult(type, result);
1273
+ return result;
1274
+ }
1275
+ cacheListResult(type, items) {
1276
+ this.listCache.set(type, { ts: Date.now(), items });
1277
+ }
1278
+ /** Internal helper: drop the cached `list()` result for a type. */
1279
+ invalidateListCache(type) {
1280
+ this.listCache.delete(type);
1343
1281
  }
1344
1282
  /**
1345
1283
  * Unregister/remove a metadata item by type and name.
@@ -1353,6 +1291,7 @@ var MetadataManager = class {
1353
1291
  this.registry.delete(type);
1354
1292
  }
1355
1293
  }
1294
+ this.invalidateListCache(type);
1356
1295
  for (const loader of this.loaders.values()) {
1357
1296
  if (loader.contract.protocol !== "datasource:" || !loader.contract.capabilities.write) continue;
1358
1297
  if (typeof loader.delete === "function") {
@@ -1767,6 +1706,14 @@ var MetadataManager = class {
1767
1706
  * Save/update an overlay for a metadata item
1768
1707
  */
1769
1708
  async saveOverlay(overlay) {
1709
+ if (this.config.persistence?.overlayWritable === false) {
1710
+ const msg = `MetadataManager overlays are read-only (persistence.overlayWritable=false); refusing to save overlay for ${overlay.baseType}/${overlay.baseName}`;
1711
+ if (this.config.validation?.throwOnError) {
1712
+ throw new Error(msg);
1713
+ }
1714
+ this.logger.warn(msg);
1715
+ return;
1716
+ }
1770
1717
  const key = this.overlayKey(overlay.baseType, overlay.baseName, overlay.scope);
1771
1718
  this.overlays.set(key, overlay);
1772
1719
  }
@@ -2236,6 +2183,8 @@ var MetadataManager = class {
2236
2183
  };
2237
2184
  }
2238
2185
  };
2186
+ _MetadataManager.LIST_CACHE_TTL_MS = 3e4;
2187
+ var MetadataManager = _MetadataManager;
2239
2188
 
2240
2189
  // src/plugin.ts
2241
2190
  import { readFile as readFile2 } from "fs/promises";
@@ -2727,13 +2676,13 @@ var MemoryLoader = class {
2727
2676
  // src/plugin.ts
2728
2677
  import { DEFAULT_METADATA_TYPE_REGISTRY } from "@objectstack/spec/kernel";
2729
2678
  import {
2730
- SysAgent,
2731
- SysFlow,
2732
- SysObject,
2733
- SysTool,
2734
- SysView
2679
+ SysMetadataObject as SysMetadataObject2,
2680
+ SysMetadataHistoryObject as SysMetadataHistoryObject2
2735
2681
  } from "@objectstack/platform-objects/metadata";
2736
- var queryableMetadataObjects = [SysObject, SysView, SysFlow, SysAgent, SysTool];
2682
+ var queryableMetadataObjects = [
2683
+ SysMetadataObject2,
2684
+ SysMetadataHistoryObject2
2685
+ ];
2737
2686
  var ARTIFACT_FIELD_TO_TYPE = {
2738
2687
  objects: "object",
2739
2688
  objectExtensions: "object_extension",
@@ -2801,13 +2750,35 @@ var MetadataPlugin = class {
2801
2750
  };
2802
2751
  this.start = async (ctx) => {
2803
2752
  const src = this.options.artifactSource;
2804
- if (src?.mode === "local-file") {
2805
- await this._loadFromLocalFile(ctx, src.path);
2806
- } else if (src?.mode === "artifact-api") {
2807
- ctx.logger.warn("[MetadataPlugin] artifact-api source is not yet implemented; falling back to file-system scan");
2808
- await this._loadFromFileSystem(ctx);
2753
+ const mode = this.options.config?.bootstrap ?? "eager";
2754
+ ctx.logger.info("[MetadataPlugin] Bootstrapping metadata", {
2755
+ bootstrap: mode,
2756
+ artifactSource: src?.mode ?? "none"
2757
+ });
2758
+ if (mode === "artifact-only") {
2759
+ if (src?.mode === "local-file") {
2760
+ await this._loadFromLocalFile(ctx, src.path, src.fetchTimeoutMs);
2761
+ } else if (src?.mode === "artifact-api") {
2762
+ await this._loadFromArtifactApi(ctx, src);
2763
+ } else {
2764
+ throw new Error("[MetadataPlugin] bootstrap=artifact-only requires options.artifactSource to be set");
2765
+ }
2766
+ } else if (mode === "lazy") {
2767
+ if (src?.mode === "local-file") {
2768
+ await this._loadFromLocalFile(ctx, src.path, src.fetchTimeoutMs);
2769
+ } else if (src?.mode === "artifact-api") {
2770
+ await this._loadFromArtifactApi(ctx, src);
2771
+ } else {
2772
+ ctx.logger.info("[MetadataPlugin] lazy bootstrap \u2014 skipping filesystem priming; metadata loads on demand");
2773
+ }
2809
2774
  } else {
2810
- await this._loadFromFileSystem(ctx);
2775
+ if (src?.mode === "local-file") {
2776
+ await this._loadFromLocalFile(ctx, src.path, src.fetchTimeoutMs);
2777
+ } else if (src?.mode === "artifact-api") {
2778
+ await this._loadFromArtifactApi(ctx, src);
2779
+ } else {
2780
+ await this._loadFromFileSystem(ctx);
2781
+ }
2811
2782
  }
2812
2783
  try {
2813
2784
  const realtimeService = ctx.getService("realtime");
@@ -2826,45 +2797,46 @@ var MetadataPlugin = class {
2826
2797
  ...options
2827
2798
  };
2828
2799
  const rootDir = this.options.rootDir || process.cwd();
2800
+ const bootstrapMode = this.options.config?.bootstrap ?? "eager";
2801
+ const effectiveWatch = bootstrapMode === "artifact-only" ? false : this.options.watch ?? true;
2829
2802
  this.manager = new NodeMetadataManager({
2830
2803
  rootDir,
2831
- watch: this.options.watch ?? true,
2804
+ watch: effectiveWatch,
2832
2805
  formats: ["yaml", "json", "typescript", "javascript"]
2833
2806
  });
2834
2807
  this.manager.setTypeRegistry(DEFAULT_METADATA_TYPE_REGISTRY);
2835
2808
  }
2836
- async _loadFromLocalFile(ctx, filePath) {
2837
- const isUrl = /^https?:\/\//i.test(filePath);
2838
- ctx.logger.info(
2839
- `[MetadataPlugin] Loading metadata from ${isUrl ? "remote URL" : "local artifact file"}`,
2840
- { path: filePath }
2841
- );
2842
- let raw;
2809
+ /**
2810
+ * Fetch JSON content from a URL with configurable timeout.
2811
+ */
2812
+ async _fetchJson(url, fetchTimeoutMs, token) {
2813
+ const envTimeout = Number(process.env.OS_ARTIFACT_FETCH_TIMEOUT_MS);
2814
+ const timeoutMs = fetchTimeoutMs ?? (Number.isFinite(envTimeout) && envTimeout > 0 ? envTimeout : void 0) ?? 6e4;
2815
+ const controller = new AbortController();
2816
+ const timer = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : void 0;
2843
2817
  try {
2844
- let content;
2845
- if (isUrl) {
2846
- const controller = new AbortController();
2847
- const timer = setTimeout(() => controller.abort(), 15e3);
2848
- try {
2849
- const res = await fetch(filePath, {
2850
- redirect: "follow",
2851
- signal: controller.signal,
2852
- headers: { Accept: "application/json, */*;q=0.5" }
2853
- });
2854
- if (!res.ok) {
2855
- throw new Error(`HTTP ${res.status} ${res.statusText}`);
2856
- }
2857
- content = await res.text();
2858
- } finally {
2859
- clearTimeout(timer);
2860
- }
2861
- } else {
2862
- content = await readFile2(filePath, "utf8");
2863
- }
2864
- raw = JSON.parse(content);
2818
+ const headers = { Accept: "application/json, */*;q=0.5" };
2819
+ if (token) headers.Authorization = `Bearer ${token}`;
2820
+ const res = await fetch(url, { redirect: "follow", signal: controller.signal, headers });
2821
+ if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
2822
+ const content = await res.text();
2823
+ return JSON.parse(content);
2865
2824
  } catch (e) {
2866
- throw new Error(`[MetadataPlugin] Cannot read artifact ${isUrl ? "URL" : "file"} at "${filePath}": ${e.message}`);
2825
+ if (e?.name === "AbortError") {
2826
+ throw new Error(
2827
+ `fetch timed out after ${timeoutMs}ms \u2014 set artifactSource.fetchTimeoutMs or OS_ARTIFACT_FETCH_TIMEOUT_MS to extend it (0 disables)`
2828
+ );
2829
+ }
2830
+ throw e;
2831
+ } finally {
2832
+ if (timer) clearTimeout(timer);
2867
2833
  }
2834
+ }
2835
+ /**
2836
+ * Parse raw artifact JSON (envelope or bare definition) and register all
2837
+ * metadata items into the MetadataManager.
2838
+ */
2839
+ async _parseAndRegisterArtifact(ctx, raw, label) {
2868
2840
  const { ProjectArtifactSchema } = await import("@objectstack/spec/cloud");
2869
2841
  const { ObjectStackDefinitionSchema } = await import("@objectstack/spec");
2870
2842
  let metadata;
@@ -2872,6 +2844,9 @@ var MetadataPlugin = class {
2872
2844
  if (obj?.schemaVersion && obj?.commitId && obj?.metadata !== void 0) {
2873
2845
  const artifact = ProjectArtifactSchema.parse(obj);
2874
2846
  metadata = artifact.metadata;
2847
+ } else if (obj?.success && obj?.data?.metadata) {
2848
+ const artifact = ProjectArtifactSchema.parse(obj.data);
2849
+ metadata = artifact.metadata;
2875
2850
  } else {
2876
2851
  const def = ObjectStackDefinitionSchema.parse(obj);
2877
2852
  const canonical = JSON.stringify(def, Object.keys(def).sort());
@@ -2904,10 +2879,51 @@ var MetadataPlugin = class {
2904
2879
  }
2905
2880
  }
2906
2881
  this.manager.registerLoader(memLoader);
2907
- ctx.logger.info("[MetadataPlugin] Artifact metadata loaded", {
2908
- path: filePath,
2909
- totalRegistered
2910
- });
2882
+ ctx.logger.info("[MetadataPlugin] Artifact metadata loaded", { source: label, totalRegistered });
2883
+ return totalRegistered;
2884
+ }
2885
+ async _loadFromLocalFile(ctx, filePath, fetchTimeoutMs) {
2886
+ const isUrl = /^https?:\/\//i.test(filePath);
2887
+ ctx.logger.info(
2888
+ `[MetadataPlugin] Loading metadata from ${isUrl ? "remote URL" : "local artifact file"}`,
2889
+ { path: filePath }
2890
+ );
2891
+ let raw;
2892
+ try {
2893
+ if (isUrl) {
2894
+ raw = await this._fetchJson(filePath, fetchTimeoutMs);
2895
+ } else {
2896
+ const content = await readFile2(filePath, "utf8");
2897
+ raw = JSON.parse(content);
2898
+ }
2899
+ } catch (e) {
2900
+ throw new Error(`[MetadataPlugin] Cannot read artifact ${isUrl ? "URL" : "file"} at "${filePath}": ${e.message}`);
2901
+ }
2902
+ await this._parseAndRegisterArtifact(ctx, raw, filePath);
2903
+ }
2904
+ /**
2905
+ * P2: Load metadata from the cloud artifact API endpoint.
2906
+ */
2907
+ async _loadFromArtifactApi(ctx, src) {
2908
+ const projectId = this.options.projectId;
2909
+ if (!projectId) {
2910
+ throw new Error("[MetadataPlugin] artifact-api source requires options.projectId to be set");
2911
+ }
2912
+ let artifactUrl = src.url.replace(/\/+$/, "");
2913
+ if (!/\/api\/v\d+\/cloud\/projects\//i.test(artifactUrl)) {
2914
+ artifactUrl = `${artifactUrl}/api/v1/cloud/projects/${projectId}/artifact`;
2915
+ }
2916
+ if (src.commitId) {
2917
+ artifactUrl += `${artifactUrl.includes("?") ? "&" : "?"}commit=${encodeURIComponent(src.commitId)}`;
2918
+ }
2919
+ ctx.logger.info("[MetadataPlugin] Loading metadata from artifact API", { url: artifactUrl });
2920
+ let raw;
2921
+ try {
2922
+ raw = await this._fetchJson(artifactUrl, src.fetchTimeoutMs, src.token);
2923
+ } catch (e) {
2924
+ throw new Error(`[MetadataPlugin] Cannot load artifact from API "${artifactUrl}": ${e.message}`);
2925
+ }
2926
+ await this._parseAndRegisterArtifact(ctx, raw, artifactUrl);
2911
2927
  }
2912
2928
  async _loadFromFileSystem(ctx) {
2913
2929
  ctx.logger.info("Loading metadata from file system...");
@@ -3038,7 +3054,7 @@ var RemoteLoader = class {
3038
3054
  };
3039
3055
 
3040
3056
  // src/index.ts
3041
- import { SysMetadataObject as SysMetadataObject2, SysMetadataHistoryObject as SysMetadataHistoryObject2 } from "@objectstack/platform-objects/metadata";
3057
+ import { SysMetadataObject as SysMetadataObject3, SysMetadataHistoryObject as SysMetadataHistoryObject3 } from "@objectstack/platform-objects/metadata";
3042
3058
 
3043
3059
  // src/routes/history-routes.ts
3044
3060
  function registerMetadataHistoryRoutes(app, metadataService) {
@@ -3430,12 +3446,11 @@ export {
3430
3446
  MemoryLoader,
3431
3447
  MetadataManager,
3432
3448
  MetadataPlugin,
3433
- MetadataProjector,
3434
3449
  migration_exports as Migration,
3435
3450
  NodeMetadataManager,
3436
3451
  RemoteLoader,
3437
- SysMetadataHistoryObject2 as SysMetadataHistoryObject,
3438
- SysMetadataObject2 as SysMetadataObject,
3452
+ SysMetadataHistoryObject3 as SysMetadataHistoryObject,
3453
+ SysMetadataObject3 as SysMetadataObject,
3439
3454
  TypeScriptSerializer,
3440
3455
  YAMLSerializer,
3441
3456
  calculateChecksum,