Skip to content

Commit ce9af34

Browse files
committed
feat: enhance DatabaseLoader to support IDataEngine for automatic datasource routing
1 parent 790ed7c commit ce9af34

File tree

3 files changed

+212
-160
lines changed

3 files changed

+212
-160
lines changed

packages/metadata/src/loaders/database-loader.ts

Lines changed: 180 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,24 @@ import type {
2121
} from '@objectstack/spec/system';
2222
import { SysMetadataObject } from '../objects/sys-metadata.object.js';
2323
import { SysMetadataHistoryObject } from '../objects/sys-metadata-history.object.js';
24-
import type { IDataDriver } from '@objectstack/spec/contracts';
24+
import type { IDataDriver, IDataEngine } from '@objectstack/spec/contracts';
2525
import type { MetadataLoader } from './loader-interface.js';
2626
import { calculateChecksum } from '../utils/metadata-history-utils.js';
2727

2828
/**
2929
* Configuration for the DatabaseLoader.
30+
*
31+
* Accepts either a raw `IDataDriver` or an `IDataEngine` (ObjectQL).
32+
* When `engine` is provided, all CRUD operations route through the engine
33+
* which handles datasource mapping automatically — no manual driver
34+
* resolution needed. Schema sync is also skipped (the engine handles it).
3035
*/
3136
export interface DatabaseLoaderOptions {
3237
/** The IDataDriver instance to use for database operations */
33-
driver: IDataDriver;
38+
driver?: IDataDriver;
39+
40+
/** The IDataEngine (ObjectQL) instance — preferred over raw driver */
41+
engine?: IDataEngine;
3442

3543
/** The table name to store metadata records (default: 'sys_metadata') */
3644
tableName?: string;
@@ -64,7 +72,8 @@ export class DatabaseLoader implements MetadataLoader {
6472
},
6573
};
6674

67-
private driver: IDataDriver;
75+
private driver?: IDataDriver;
76+
private engine?: IDataEngine;
6877
private tableName: string;
6978
private historyTableName: string;
7079
private tenantId?: string;
@@ -73,13 +82,63 @@ export class DatabaseLoader implements MetadataLoader {
7382
private historySchemaReady = false;
7483

7584
constructor(options: DatabaseLoaderOptions) {
85+
if (!options.driver && !options.engine) {
86+
throw new Error('DatabaseLoader requires either a driver or engine');
87+
}
7688
this.driver = options.driver;
89+
this.engine = options.engine;
7790
this.tableName = options.tableName ?? 'sys_metadata';
7891
this.historyTableName = options.historyTableName ?? 'sys_metadata_history';
7992
this.tenantId = options.tenantId;
8093
this.trackHistory = options.trackHistory !== false; // Default to true
8194
}
8295

96+
// ==========================================
97+
// Internal CRUD helpers (driver vs engine)
98+
// ==========================================
99+
100+
private async _find(table: string, query: Record<string, unknown>): Promise<Record<string, unknown>[]> {
101+
if (this.engine) {
102+
return this.engine.find(table, query as any);
103+
}
104+
return this.driver!.find(table, { object: table, ...query } as any);
105+
}
106+
107+
private async _findOne(table: string, query: Record<string, unknown>): Promise<Record<string, unknown> | null> {
108+
if (this.engine) {
109+
return this.engine.findOne(table, query as any);
110+
}
111+
return this.driver!.findOne(table, { object: table, ...query } as any);
112+
}
113+
114+
private async _count(table: string, query: Record<string, unknown>): Promise<number> {
115+
if (this.engine) {
116+
return this.engine.count(table, query as any);
117+
}
118+
return this.driver!.count(table, { object: table, ...query } as any);
119+
}
120+
121+
private async _create(table: string, data: Record<string, unknown>): Promise<Record<string, unknown>> {
122+
if (this.engine) {
123+
return this.engine.insert(table, data);
124+
}
125+
return this.driver!.create(table, data);
126+
}
127+
128+
private async _update(table: string, id: string, data: Record<string, unknown>): Promise<Record<string, unknown>> {
129+
if (this.engine) {
130+
return this.engine.update(table, { id, ...data });
131+
}
132+
return this.driver!.update(table, id, data);
133+
}
134+
135+
private async _delete(table: string, id: string): Promise<any> {
136+
if (this.engine) {
137+
return this.engine.delete(table, { where: { id } } as any);
138+
}
139+
return this.driver!.delete(table, id);
140+
}
141+
83142
/**
84143
* Ensure the metadata table exists.
85144
* Uses IDataDriver.syncSchema with the SysMetadataObject definition
@@ -88,8 +147,14 @@ export class DatabaseLoader implements MetadataLoader {
88147
private async ensureSchema(): Promise<void> {
89148
if (this.schemaReady) return;
90149

150+
// When using engine, schema sync is handled by ObjectQL startup
151+
if (this.engine) {
152+
this.schemaReady = true;
153+
return;
154+
}
155+
91156
try {
92-
await this.driver.syncSchema(this.tableName, {
157+
await this.driver!.syncSchema(this.tableName, {
93158
...SysMetadataObject,
94159
name: this.tableName,
95160
});
@@ -107,8 +172,14 @@ export class DatabaseLoader implements MetadataLoader {
107172
private async ensureHistorySchema(): Promise<void> {
108173
if (!this.trackHistory || this.historySchemaReady) return;
109174

175+
// When using engine, schema sync is handled by ObjectQL startup
176+
if (this.engine) {
177+
this.historySchemaReady = true;
178+
return;
179+
}
180+
110181
try {
111-
await this.driver.syncSchema(this.historyTableName, {
182+
await this.driver!.syncSchema(this.historyTableName, {
112183
...SysMetadataHistoryObject,
113184
name: this.historyTableName,
114185
});
@@ -191,7 +262,7 @@ export class DatabaseLoader implements MetadataLoader {
191262
};
192263

193264
try {
194-
await this.driver.create(this.historyTableName, {
265+
await this._create(this.historyTableName, {
195266
id: historyRecord.id,
196267
metadata_id: historyRecord.metadataId,
197268
name: historyRecord.name,
@@ -269,8 +340,7 @@ export class DatabaseLoader implements MetadataLoader {
269340
await this.ensureSchema();
270341

271342
try {
272-
const row = await this.driver.findOne(this.tableName, {
273-
object: this.tableName,
343+
const row = await this._findOne(this.tableName, {
274344
where: this.baseFilter(type, name),
275345
});
276346

@@ -306,8 +376,7 @@ export class DatabaseLoader implements MetadataLoader {
306376
await this.ensureSchema();
307377

308378
try {
309-
const rows = await this.driver.find(this.tableName, {
310-
object: this.tableName,
379+
const rows = await this._find(this.tableName, {
311380
where: this.baseFilter(type),
312381
});
313382

@@ -323,8 +392,7 @@ export class DatabaseLoader implements MetadataLoader {
323392
await this.ensureSchema();
324393

325394
try {
326-
const count = await this.driver.count(this.tableName, {
327-
object: this.tableName,
395+
const count = await this._count(this.tableName, {
328396
where: this.baseFilter(type, name),
329397
});
330398

@@ -338,8 +406,7 @@ export class DatabaseLoader implements MetadataLoader {
338406
await this.ensureSchema();
339407

340408
try {
341-
const row = await this.driver.findOne(this.tableName, {
342-
object: this.tableName,
409+
const row = await this._findOne(this.tableName, {
343410
where: this.baseFilter(type, name),
344411
});
345412

@@ -365,8 +432,7 @@ export class DatabaseLoader implements MetadataLoader {
365432
await this.ensureSchema();
366433

367434
try {
368-
const rows = await this.driver.find(this.tableName, {
369-
object: this.tableName,
435+
const rows = await this._find(this.tableName, {
370436
where: this.baseFilter(type),
371437
fields: ['name'],
372438
});
@@ -393,8 +459,7 @@ export class DatabaseLoader implements MetadataLoader {
393459
await this.ensureHistorySchema();
394460

395461
// Resolve the parent metadata record ID
396-
const metadataRow = await this.driver.findOne(this.tableName, {
397-
object: this.tableName,
462+
const metadataRow = await this._findOne(this.tableName, {
398463
where: this.baseFilter(type, name),
399464
});
400465
if (!metadataRow) return null;
@@ -407,8 +472,7 @@ export class DatabaseLoader implements MetadataLoader {
407472
filter.tenant_id = this.tenantId;
408473
}
409474

410-
const row = await this.driver.findOne(this.historyTableName, {
411-
object: this.historyTableName,
475+
const row = await this._findOne(this.historyTableName, {
412476
where: filter,
413477
});
414478
if (!row) return null;
@@ -430,6 +494,95 @@ export class DatabaseLoader implements MetadataLoader {
430494
};
431495
}
432496

497+
/**
498+
* Query history records with pagination and filtering.
499+
* Encapsulates history table queries so MetadataManager doesn't need
500+
* direct driver access.
501+
*/
502+
async queryHistory(
503+
type: string,
504+
name: string,
505+
options?: {
506+
operationType?: string;
507+
since?: string;
508+
until?: string;
509+
limit?: number;
510+
offset?: number;
511+
includeMetadata?: boolean;
512+
}
513+
): Promise<{ records: any[]; total: number; hasMore: boolean }> {
514+
if (!this.trackHistory) {
515+
return { records: [], total: 0, hasMore: false };
516+
}
517+
518+
await this.ensureSchema();
519+
await this.ensureHistorySchema();
520+
521+
// Find the metadata record
522+
const filter: Record<string, unknown> = { type, name };
523+
if (this.tenantId) filter.tenant_id = this.tenantId;
524+
525+
const metadataRecord = await this._findOne(this.tableName, { where: filter });
526+
if (!metadataRecord) {
527+
return { records: [], total: 0, hasMore: false };
528+
}
529+
530+
// Build history query
531+
const historyFilter: Record<string, unknown> = {
532+
metadata_id: metadataRecord.id,
533+
};
534+
if (this.tenantId) historyFilter.tenant_id = this.tenantId;
535+
if (options?.operationType) historyFilter.operation_type = options.operationType;
536+
if (options?.since) historyFilter.recorded_at = { $gte: options.since };
537+
if (options?.until) {
538+
if (historyFilter.recorded_at) {
539+
(historyFilter.recorded_at as Record<string, unknown>).$lte = options.until;
540+
} else {
541+
historyFilter.recorded_at = { $lte: options.until };
542+
}
543+
}
544+
545+
const limit = options?.limit ?? 50;
546+
const offset = options?.offset ?? 0;
547+
548+
const historyRecords = await this._find(this.historyTableName, {
549+
where: historyFilter,
550+
orderBy: [{ field: 'recorded_at', order: 'desc' as const }],
551+
limit: limit + 1,
552+
offset,
553+
});
554+
555+
const hasMore = historyRecords.length > limit;
556+
const records = historyRecords.slice(0, limit);
557+
const total = await this._count(this.historyTableName, { where: historyFilter });
558+
559+
const includeMetadata = options?.includeMetadata !== false;
560+
const result = records.map((row: Record<string, unknown>) => {
561+
const parsedMetadata =
562+
typeof row.metadata === 'string'
563+
? JSON.parse(row.metadata as string)
564+
: (row.metadata as Record<string, unknown> | null | undefined);
565+
566+
return {
567+
id: row.id as string,
568+
metadataId: row.metadata_id as string,
569+
name: row.name as string,
570+
type: row.type as string,
571+
version: row.version as number,
572+
operationType: row.operation_type as string,
573+
metadata: includeMetadata ? parsedMetadata : null,
574+
checksum: row.checksum as string,
575+
previousChecksum: row.previous_checksum as string | undefined,
576+
changeNote: row.change_note as string | undefined,
577+
tenantId: row.tenant_id as string | undefined,
578+
recordedBy: row.recorded_by as string | undefined,
579+
recordedAt: row.recorded_at as string,
580+
};
581+
});
582+
583+
return { records: result, total, hasMore };
584+
}
585+
433586
/**
434587
* Perform a rollback: persist `restoredData` as the new current state and record a
435588
* single 'revert' history entry (instead of the usual 'update' entry that `save()`
@@ -451,8 +604,7 @@ export class DatabaseLoader implements MetadataLoader {
451604
const metadataJson = JSON.stringify(restoredData);
452605
const newChecksum = await calculateChecksum(restoredData);
453606

454-
const existing = await this.driver.findOne(this.tableName, {
455-
object: this.tableName,
607+
const existing = await this._findOne(this.tableName, {
456608
where: this.baseFilter(type, name),
457609
});
458610

@@ -463,7 +615,7 @@ export class DatabaseLoader implements MetadataLoader {
463615
const previousChecksum = existing.checksum as string | undefined;
464616
const newVersion = ((existing.version as number) ?? 0) + 1;
465617

466-
await this.driver.update(this.tableName, existing.id as string, {
618+
await this._update(this.tableName, existing.id as string, {
467619
metadata: metadataJson,
468620
version: newVersion,
469621
checksum: newChecksum,
@@ -500,8 +652,7 @@ export class DatabaseLoader implements MetadataLoader {
500652
const newChecksum = await calculateChecksum(data);
501653

502654
try {
503-
const existing = await this.driver.findOne(this.tableName, {
504-
object: this.tableName,
655+
const existing = await this._findOne(this.tableName, {
505656
where: this.baseFilter(type, name),
506657
});
507658

@@ -520,7 +671,7 @@ export class DatabaseLoader implements MetadataLoader {
520671
// Update existing record
521672
const version = ((existing.version as number) ?? 0) + 1;
522673

523-
await this.driver.update(this.tableName, existing.id as string, {
674+
await this._update(this.tableName, existing.id as string, {
524675
metadata: metadataJson,
525676
version,
526677
checksum: newChecksum,
@@ -548,7 +699,7 @@ export class DatabaseLoader implements MetadataLoader {
548699
} else {
549700
// Create new record
550701
const id = generateId();
551-
await this.driver.create(this.tableName, {
702+
await this._create(this.tableName, {
552703
id,
553704
name,
554705
type,
@@ -598,8 +749,7 @@ export class DatabaseLoader implements MetadataLoader {
598749
await this.ensureSchema();
599750

600751
// Find the existing record to get its ID
601-
const existing = await this.driver.findOne(this.tableName, {
602-
object: this.tableName,
752+
const existing = await this._findOne(this.tableName, {
603753
where: this.baseFilter(type, name),
604754
});
605755

@@ -609,7 +759,7 @@ export class DatabaseLoader implements MetadataLoader {
609759
}
610760

611761
// Delete from the main metadata table using the record's ID
612-
await this.driver.delete(this.tableName, existing.id as string);
762+
await this._delete(this.tableName, existing.id as string);
613763
}
614764
}
615765

0 commit comments

Comments
 (0)