diff --git a/packages/drivers/excel/src/index.ts b/packages/drivers/excel/src/index.ts index 03bc8c07..2748ccfa 100644 --- a/packages/drivers/excel/src/index.ts +++ b/packages/drivers/excel/src/index.ts @@ -72,8 +72,23 @@ export interface ExcelDriverConfig { * in a separate worksheet, with the first row containing column headers. * * Uses ExcelJS library for secure Excel file operations. + * + * Implements both the legacy Driver interface from @objectql/types and + * the standard DriverInterface from @objectstack/spec for compatibility + * with the new kernel-based plugin system. */ export class ExcelDriver implements Driver { + // Driver metadata (ObjectStack-compatible) + public readonly name = 'ExcelDriver'; + public readonly version = '3.0.1'; + public readonly supports = { + transactions: false, + joins: false, + fullTextSearch: false, + jsonFields: true, + arrayFields: true + }; + private config: ExcelDriverConfig; private workbook!: ExcelJS.Workbook; private workbooks: Map; // For file-per-object mode @@ -114,6 +129,46 @@ export class ExcelDriver implements Driver { await this.loadWorkbook(); } + /** + * Connect to the database (for DriverInterface compatibility) + * This calls init() to load the workbook. + */ + async connect(): Promise { + await this.init(); + } + + /** + * Check database connection health + */ + async checkHealth(): Promise { + try { + if (this.fileStorageMode === 'single-file') { + // Check if file exists or can be created + if (!fs.existsSync(this.filePath)) { + if (!this.config.createIfMissing) { + return false; + } + // Check if directory is writable + const dir = path.dirname(this.filePath); + if (!fs.existsSync(dir)) { + return false; + } + } + return true; + } else { + // Check if directory exists or can be created + if (!fs.existsSync(this.filePath)) { + if (!this.config.createIfMissing) { + return false; + } + } + return true; + } + } catch (error) { + return false; + } + } + /** * Factory method to create and initialize the driver. */ @@ -515,6 +570,8 @@ export class ExcelDriver implements Driver { * Find multiple records matching the query criteria. */ async find(objectName: string, query: any = {}, options?: any): Promise { + // Normalize query to support both legacy and QueryAST formats + const normalizedQuery = this.normalizeQuery(query); let results = this.data.get(objectName) || []; // Return empty array if no data @@ -526,26 +583,26 @@ export class ExcelDriver implements Driver { results = results.map(r => ({ ...r })); // Apply filters - if (query.filters) { - results = this.applyFilters(results, query.filters); + if (normalizedQuery.filters) { + results = this.applyFilters(results, normalizedQuery.filters); } // Apply sorting - if (query.sort && Array.isArray(query.sort)) { - results = this.applySort(results, query.sort); + if (normalizedQuery.sort && Array.isArray(normalizedQuery.sort)) { + results = this.applySort(results, normalizedQuery.sort); } // Apply pagination - if (query.skip) { - results = results.slice(query.skip); + if (normalizedQuery.skip) { + results = results.slice(normalizedQuery.skip); } - if (query.limit) { - results = results.slice(0, query.limit); + if (normalizedQuery.limit) { + results = results.slice(0, normalizedQuery.limit); } // Apply field projection - if (query.fields && Array.isArray(query.fields)) { - results = results.map(doc => this.projectFields(doc, query.fields)); + if (normalizedQuery.fields && Array.isArray(normalizedQuery.fields)) { + results = results.map(doc => this.projectFields(doc, normalizedQuery.fields)); } return results; @@ -794,6 +851,39 @@ export class ExcelDriver implements Driver { // ========== Helper Methods ========== + /** + * Normalizes query format to support both legacy UnifiedQuery and QueryAST formats. + * This ensures backward compatibility while supporting the new @objectstack/spec interface. + * + * QueryAST format uses 'top' for limit, while UnifiedQuery uses 'limit'. + * QueryAST sort is array of {field, order}, while UnifiedQuery is array of [field, order]. + */ + private normalizeQuery(query: any): any { + if (!query) return {}; + + const normalized: any = { ...query }; + + // Normalize limit/top + if (normalized.top !== undefined && normalized.limit === undefined) { + normalized.limit = normalized.top; + } + + // Normalize sort format + if (normalized.sort && Array.isArray(normalized.sort)) { + // Check if it's already in the array format [field, order] + const firstSort = normalized.sort[0]; + if (firstSort && typeof firstSort === 'object' && !Array.isArray(firstSort)) { + // Convert from QueryAST format {field, order} to internal format [field, order] + normalized.sort = normalized.sort.map((item: any) => [ + item.field, + item.order || item.direction || item.dir || 'asc' + ]); + } + } + + return normalized; + } + /** * Apply filters to an array of records (in-memory filtering). */ diff --git a/packages/drivers/fs/src/index.ts b/packages/drivers/fs/src/index.ts index 696ef5d5..6bfa88cd 100644 --- a/packages/drivers/fs/src/index.ts +++ b/packages/drivers/fs/src/index.ts @@ -12,6 +12,10 @@ * A persistent file-based driver for ObjectQL that stores data in JSON files. * Each object type is stored in a separate JSON file for easy inspection and backup. * + * Implements both the legacy Driver interface from @objectql/types and + * the standard DriverInterface from @objectstack/spec for compatibility + * with the new kernel-based plugin system. + * * ✅ Production-ready features: * - Persistent storage with JSON files * - One file per table/object (e.g., users.json, projects.json) @@ -56,6 +60,17 @@ export interface FileSystemDriverConfig { * - Content: Array of records `[{id: "1", ...}, {id: "2", ...}]` */ export class FileSystemDriver implements Driver { + // Driver metadata (ObjectStack-compatible) + public readonly name = 'FileSystemDriver'; + public readonly version = '3.0.1'; + public readonly supports = { + transactions: false, + joins: false, + fullTextSearch: false, + jsonFields: true, + arrayFields: true + }; + private config: FileSystemDriverConfig; private idCounters: Map; private cache: Map; @@ -81,6 +96,31 @@ export class FileSystemDriver implements Driver { } } + /** + * Connect to the database (for DriverInterface compatibility) + * This is a no-op for filesystem driver as there's no external connection. + */ + async connect(): Promise { + // No-op: FileSystem driver doesn't need connection + } + + /** + * Check database connection health + */ + async checkHealth(): Promise { + try { + // Check if data directory is accessible + if (!fs.existsSync(this.config.dataDir)) { + return false; + } + // Try to read directory + fs.readdirSync(this.config.dataDir); + return true; + } catch (error) { + return false; + } + } + /** * Load initial data into the store. */ @@ -217,29 +257,31 @@ export class FileSystemDriver implements Driver { * Find multiple records matching the query criteria. */ async find(objectName: string, query: any = {}, options?: any): Promise { + // Normalize query to support both legacy and QueryAST formats + const normalizedQuery = this.normalizeQuery(query); let results = this.loadRecords(objectName); // Apply filters - if (query.filters) { - results = this.applyFilters(results, query.filters); + if (normalizedQuery.filters) { + results = this.applyFilters(results, normalizedQuery.filters); } // Apply sorting - if (query.sort && Array.isArray(query.sort)) { - results = this.applySort(results, query.sort); + if (normalizedQuery.sort && Array.isArray(normalizedQuery.sort)) { + results = this.applySort(results, normalizedQuery.sort); } // Apply pagination - if (query.skip) { - results = results.slice(query.skip); + if (normalizedQuery.skip) { + results = results.slice(normalizedQuery.skip); } - if (query.limit) { - results = results.slice(0, query.limit); + if (normalizedQuery.limit) { + results = results.slice(0, normalizedQuery.limit); } // Apply field projection - if (query.fields && Array.isArray(query.fields)) { - results = results.map(doc => this.projectFields(doc, query.fields)); + if (normalizedQuery.fields && Array.isArray(normalizedQuery.fields)) { + results = results.map(doc => this.projectFields(doc, normalizedQuery.fields)); } // Return deep copies to prevent external modifications @@ -530,6 +572,39 @@ export class FileSystemDriver implements Driver { // ========== Helper Methods ========== + /** + * Normalizes query format to support both legacy UnifiedQuery and QueryAST formats. + * This ensures backward compatibility while supporting the new @objectstack/spec interface. + * + * QueryAST format uses 'top' for limit, while UnifiedQuery uses 'limit'. + * QueryAST sort is array of {field, order}, while UnifiedQuery is array of [field, order]. + */ + private normalizeQuery(query: any): any { + if (!query) return {}; + + const normalized: any = { ...query }; + + // Normalize limit/top + if (normalized.top !== undefined && normalized.limit === undefined) { + normalized.limit = normalized.top; + } + + // Normalize sort format + if (normalized.sort && Array.isArray(normalized.sort)) { + // Check if it's already in the array format [field, order] + const firstSort = normalized.sort[0]; + if (firstSort && typeof firstSort === 'object' && !Array.isArray(firstSort)) { + // Convert from QueryAST format {field, order} to internal format [field, order] + normalized.sort = normalized.sort.map((item: any) => [ + item.field, + item.order || item.direction || item.dir || 'asc' + ]); + } + } + + return normalized; + } + /** * Apply filters to an array of records. * diff --git a/packages/drivers/localstorage/src/index.ts b/packages/drivers/localstorage/src/index.ts index 0470415d..190b07f5 100644 --- a/packages/drivers/localstorage/src/index.ts +++ b/packages/drivers/localstorage/src/index.ts @@ -12,6 +12,10 @@ * A browser-based driver that persists data to localStorage. * Perfect for client-side applications that need persistence across sessions. * + * Implements both the legacy Driver interface from @objectql/types and + * the standard DriverInterface from @objectstack/spec for compatibility + * with the new kernel-based plugin system. + * * ✅ Production-ready features: * - Browser localStorage persistence * - Automatic serialization/deserialization @@ -52,6 +56,17 @@ export interface LocalStorageDriverConfig { * Example: `objectql:users:user-123` → `{"id":"user-123","name":"Alice",...}` */ export class LocalStorageDriver implements Driver { + // Driver metadata (ObjectStack-compatible) + public readonly name = 'LocalStorageDriver'; + public readonly version = '3.0.1'; + public readonly supports = { + transactions: false, + joins: false, + fullTextSearch: false, + jsonFields: true, + arrayFields: true + }; + private config: LocalStorageDriverConfig; private storage: Storage; private namespace: string; @@ -81,6 +96,34 @@ export class LocalStorageDriver implements Driver { } } + /** + * Connect to the database (for DriverInterface compatibility) + * This is a no-op for localStorage driver as there's no external connection. + */ + async connect(): Promise { + // No-op: LocalStorage driver doesn't need connection + } + + /** + * Check database connection health + */ + async checkHealth(): Promise { + try { + // Check if localStorage is accessible + if (!this.storage) { + return false; + } + // Try a test write and read + const testKey = `${this.namespace}:healthcheck`; + this.storage.setItem(testKey, 'ok'); + const value = this.storage.getItem(testKey); + this.storage.removeItem(testKey); + return value === 'ok'; + } catch (error) { + return false; + } + } + /** * Load initial data into localStorage. */ @@ -140,6 +183,8 @@ export class LocalStorageDriver implements Driver { * Find multiple records matching the query criteria. */ async find(objectName: string, query: any = {}, options?: any): Promise { + // Normalize query to support both legacy and QueryAST formats + const normalizedQuery = this.normalizeQuery(query); const keys = this.getObjectKeys(objectName); let results: any[] = []; @@ -156,26 +201,26 @@ export class LocalStorageDriver implements Driver { } // Apply filters - if (query.filters) { - results = this.applyFilters(results, query.filters); + if (normalizedQuery.filters) { + results = this.applyFilters(results, normalizedQuery.filters); } // Apply sorting - if (query.sort && Array.isArray(query.sort)) { - results = this.applySort(results, query.sort); + if (normalizedQuery.sort && Array.isArray(normalizedQuery.sort)) { + results = this.applySort(results, normalizedQuery.sort); } // Apply pagination - if (query.skip) { - results = results.slice(query.skip); + if (normalizedQuery.skip) { + results = results.slice(normalizedQuery.skip); } - if (query.limit) { - results = results.slice(0, query.limit); + if (normalizedQuery.limit) { + results = results.slice(0, normalizedQuery.limit); } // Apply field projection - if (query.fields && Array.isArray(query.fields)) { - results = results.map(doc => this.projectFields(doc, query.fields)); + if (normalizedQuery.fields && Array.isArray(normalizedQuery.fields)) { + results = results.map(doc => this.projectFields(doc, normalizedQuery.fields)); } return results; @@ -493,6 +538,39 @@ export class LocalStorageDriver implements Driver { // ========== Helper Methods (Same as MemoryDriver) ========== + /** + * Normalizes query format to support both legacy UnifiedQuery and QueryAST formats. + * This ensures backward compatibility while supporting the new @objectstack/spec interface. + * + * QueryAST format uses 'top' for limit, while UnifiedQuery uses 'limit'. + * QueryAST sort is array of {field, order}, while UnifiedQuery is array of [field, order]. + */ + private normalizeQuery(query: any): any { + if (!query) return {}; + + const normalized: any = { ...query }; + + // Normalize limit/top + if (normalized.top !== undefined && normalized.limit === undefined) { + normalized.limit = normalized.top; + } + + // Normalize sort format + if (normalized.sort && Array.isArray(normalized.sort)) { + // Check if it's already in the array format [field, order] + const firstSort = normalized.sort[0]; + if (firstSort && typeof firstSort === 'object' && !Array.isArray(firstSort)) { + // Convert from QueryAST format {field, order} to internal format [field, order] + normalized.sort = normalized.sort.map((item: any) => [ + item.field, + item.order || item.direction || item.dir || 'asc' + ]); + } + } + + return normalized; + } + private applyFilters(records: any[], filters: any[]): any[] { if (!filters || filters.length === 0) { return records; diff --git a/packages/drivers/memory/src/index.ts b/packages/drivers/memory/src/index.ts index fdeb2662..797bbda2 100644 --- a/packages/drivers/memory/src/index.ts +++ b/packages/drivers/memory/src/index.ts @@ -12,6 +12,10 @@ * A high-performance in-memory driver for ObjectQL that stores data in JavaScript Maps. * Perfect for testing, development, and environments where persistence is not required. * + * Implements both the legacy Driver interface from @objectql/types and + * the standard DriverInterface from @objectstack/spec for compatibility + * with the new kernel-based plugin system. + * * ✅ Production-ready features: * - Zero external dependencies * - Thread-safe operations @@ -48,6 +52,17 @@ export interface MemoryDriverConfig { * Example: `users:user-123` → `{id: "user-123", name: "Alice", ...}` */ export class MemoryDriver implements Driver { + // Driver metadata (ObjectStack-compatible) + public readonly name = 'MemoryDriver'; + public readonly version = '3.0.1'; + public readonly supports = { + transactions: false, + joins: false, + fullTextSearch: false, + jsonFields: true, + arrayFields: true + }; + private store: Map; private config: MemoryDriverConfig; private idCounters: Map; @@ -63,6 +78,22 @@ export class MemoryDriver implements Driver { } } + /** + * Connect to the database (for DriverInterface compatibility) + * This is a no-op for memory driver as there's no external connection. + */ + async connect(): Promise { + // No-op: Memory driver doesn't need connection + } + + /** + * Check database connection health + */ + async checkHealth(): Promise { + // Memory driver is always healthy if it exists + return true; + } + /** * Load initial data into the store. */ @@ -81,6 +112,9 @@ export class MemoryDriver implements Driver { * Supports filtering, sorting, pagination, and field projection. */ async find(objectName: string, query: any = {}, options?: any): Promise { + // Normalize query to support both legacy and QueryAST formats + const normalizedQuery = this.normalizeQuery(query); + // Get all records for this object type const pattern = `${objectName}:`; let results: any[] = []; @@ -92,26 +126,26 @@ export class MemoryDriver implements Driver { } // Apply filters - if (query.filters) { - results = this.applyFilters(results, query.filters); + if (normalizedQuery.filters) { + results = this.applyFilters(results, normalizedQuery.filters); } // Apply sorting - if (query.sort && Array.isArray(query.sort)) { - results = this.applySort(results, query.sort); + if (normalizedQuery.sort && Array.isArray(normalizedQuery.sort)) { + results = this.applySort(results, normalizedQuery.sort); } // Apply pagination - if (query.skip) { - results = results.slice(query.skip); + if (normalizedQuery.skip) { + results = results.slice(normalizedQuery.skip); } - if (query.limit) { - results = results.slice(0, query.limit); + if (normalizedQuery.limit) { + results = results.slice(0, normalizedQuery.limit); } // Apply field projection - if (query.fields && Array.isArray(query.fields)) { - results = results.map(doc => this.projectFields(doc, query.fields)); + if (normalizedQuery.fields && Array.isArray(normalizedQuery.fields)) { + results = results.map(doc => this.projectFields(doc, normalizedQuery.fields)); } return results; @@ -354,6 +388,39 @@ export class MemoryDriver implements Driver { // ========== Helper Methods ========== + /** + * Normalizes query format to support both legacy UnifiedQuery and QueryAST formats. + * This ensures backward compatibility while supporting the new @objectstack/spec interface. + * + * QueryAST format uses 'top' for limit, while UnifiedQuery uses 'limit'. + * QueryAST sort is array of {field, order}, while UnifiedQuery is array of [field, order]. + */ + private normalizeQuery(query: any): any { + if (!query) return {}; + + const normalized: any = { ...query }; + + // Normalize limit/top + if (normalized.top !== undefined && normalized.limit === undefined) { + normalized.limit = normalized.top; + } + + // Normalize sort format + if (normalized.sort && Array.isArray(normalized.sort)) { + // Check if it's already in the array format [field, order] + const firstSort = normalized.sort[0]; + if (firstSort && typeof firstSort === 'object' && !Array.isArray(firstSort)) { + // Convert from QueryAST format {field, order} to internal format [field, order] + normalized.sort = normalized.sort.map((item: any) => [ + item.field, + item.order || item.direction || item.dir || 'asc' + ]); + } + } + + return normalized; + } + /** * Apply filters to an array of records (in-memory filtering). * diff --git a/packages/drivers/redis/src/index.ts b/packages/drivers/redis/src/index.ts index a497d032..2a28aceb 100644 --- a/packages/drivers/redis/src/index.ts +++ b/packages/drivers/redis/src/index.ts @@ -12,6 +12,10 @@ * This is a reference implementation demonstrating how to create a custom ObjectQL driver. * It adapts Redis (a key-value store) to work with ObjectQL's universal data protocol. * + * Implements both the legacy Driver interface from @objectql/types and + * the standard DriverInterface from @objectstack/spec for compatibility + * with the new kernel-based plugin system. + * * ⚠️ WARNING: This is an educational example, not production-ready. * It uses full key scanning which is inefficient for large datasets. * @@ -46,6 +50,17 @@ export interface RedisDriverConfig { * Example: `users:user-123` → `{"id":"user-123","name":"Alice",...}` */ export class RedisDriver implements Driver { + // Driver metadata (ObjectStack-compatible) + public readonly name = 'RedisDriver'; + public readonly version = '3.0.1'; + public readonly supports = { + transactions: false, + joins: false, + fullTextSearch: false, + jsonFields: true, + arrayFields: true + }; + private client: RedisClientType; private config: RedisDriverConfig; private connected: Promise; @@ -69,6 +84,19 @@ export class RedisDriver implements Driver { await this.client.connect(); } + /** + * Check database connection health + */ + async checkHealth(): Promise { + try { + await this.connected; + await this.client.ping(); + return true; + } catch (error) { + return false; + } + } + /** * Find multiple records matching the query criteria. * @@ -78,6 +106,9 @@ export class RedisDriver implements Driver { async find(objectName: string, query: any = {}, options?: any): Promise { await this.connected; + // Normalize query to support both legacy and QueryAST formats + const normalizedQuery = this.normalizeQuery(query); + // Get all keys for this object type const pattern = `${objectName}:*`; const keys = await this.client.keys(pattern); @@ -97,26 +128,26 @@ export class RedisDriver implements Driver { } // Apply filters (in-memory) - if (query.filters) { - results = this.applyFilters(results, query.filters); + if (normalizedQuery.filters) { + results = this.applyFilters(results, normalizedQuery.filters); } // Apply sorting (in-memory) - if (query.sort && Array.isArray(query.sort)) { - results = this.applySort(results, query.sort); + if (normalizedQuery.sort && Array.isArray(normalizedQuery.sort)) { + results = this.applySort(results, normalizedQuery.sort); } // Apply pagination - if (query.skip) { - results = results.slice(query.skip); + if (normalizedQuery.skip) { + results = results.slice(normalizedQuery.skip); } - if (query.limit) { - results = results.slice(0, query.limit); + if (normalizedQuery.limit) { + results = results.slice(0, normalizedQuery.limit); } // Apply field projection - if (query.fields && Array.isArray(query.fields)) { - results = results.map(doc => this.projectFields(doc, query.fields)); + if (normalizedQuery.fields && Array.isArray(normalizedQuery.fields)) { + results = results.map(doc => this.projectFields(doc, normalizedQuery.fields)); } return results; @@ -266,6 +297,39 @@ export class RedisDriver implements Driver { // ========== Helper Methods ========== + /** + * Normalizes query format to support both legacy UnifiedQuery and QueryAST formats. + * This ensures backward compatibility while supporting the new @objectstack/spec interface. + * + * QueryAST format uses 'top' for limit, while UnifiedQuery uses 'limit'. + * QueryAST sort is array of {field, order}, while UnifiedQuery is array of [field, order]. + */ + private normalizeQuery(query: any): any { + if (!query) return {}; + + const normalized: any = { ...query }; + + // Normalize limit/top + if (normalized.top !== undefined && normalized.limit === undefined) { + normalized.limit = normalized.top; + } + + // Normalize sort format + if (normalized.sort && Array.isArray(normalized.sort)) { + // Check if it's already in the array format [field, order] + const firstSort = normalized.sort[0]; + if (firstSort && typeof firstSort === 'object' && !Array.isArray(firstSort)) { + // Convert from QueryAST format {field, order} to internal format [field, order] + normalized.sort = normalized.sort.map((item: any) => [ + item.field, + item.order || item.direction || item.dir || 'asc' + ]); + } + } + + return normalized; + } + /** * Apply filters to an array of records (in-memory filtering). * diff --git a/packages/drivers/sdk/src/index.ts b/packages/drivers/sdk/src/index.ts index 9bd12bfa..5687ba1d 100644 --- a/packages/drivers/sdk/src/index.ts +++ b/packages/drivers/sdk/src/index.ts @@ -78,14 +78,66 @@ function createTimeoutSignal(ms: number): AbortSignal { /** * Legacy Driver implementation that uses JSON-RPC style API + * + * Implements both the legacy Driver interface from @objectql/types and + * the standard DriverInterface from @objectstack/spec for compatibility + * with the new kernel-based plugin system. */ export class RemoteDriver implements Driver { + // Driver metadata (ObjectStack-compatible) + public readonly name = 'RemoteDriver'; + public readonly version = '3.0.1'; + public readonly supports = { + transactions: false, + joins: false, + fullTextSearch: false, + jsonFields: true, + arrayFields: true + }; + private rpcPath: string; + private baseUrl: string; - constructor(private baseUrl: string, rpcPath: string = '/api/objectql') { + constructor(baseUrl: string, rpcPath: string = '/api/objectql') { + this.baseUrl = baseUrl; this.rpcPath = rpcPath; } + /** + * Connect to the remote server (for DriverInterface compatibility) + */ + async connect(): Promise { + // Test connection with a simple health check + try { + await this.checkHealth(); + } catch (error) { + throw new Error(`Failed to connect to remote server: ${(error as Error).message}`); + } + } + + /** + * Check remote server connection health + */ + async checkHealth(): Promise { + try { + const endpoint = `${this.baseUrl.replace(/\/$/, '')}${this.rpcPath}`; + const res = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + op: 'ping', + object: '_health', + args: {} + }) + }); + return res.ok; + } catch (error) { + return false; + } + } + private async request(op: string, objectName: string, args: any) { // Implementation detail: we assume there is a standard endpoint at rpcPath // that accepts the ObjectQLRequest format. @@ -112,12 +164,47 @@ export class RemoteDriver implements Driver { return json.data; } + /** + * Normalizes query format to support both legacy UnifiedQuery and QueryAST formats. + * This ensures backward compatibility while supporting the new @objectstack/spec interface. + * + * QueryAST format uses 'top' for limit, while UnifiedQuery uses 'limit'. + * QueryAST sort is array of {field, order}, while UnifiedQuery is array of [field, order]. + */ + private normalizeQuery(query: any): any { + if (!query) return {}; + + const normalized: any = { ...query }; + + // Normalize limit/top + if (normalized.top !== undefined && normalized.limit === undefined) { + normalized.limit = normalized.top; + } + + // Normalize sort format + if (normalized.sort && Array.isArray(normalized.sort)) { + // Check if it's already in the array format [field, order] + const firstSort = normalized.sort[0]; + if (firstSort && typeof firstSort === 'object' && !Array.isArray(firstSort)) { + // Convert from QueryAST format {field, order} to internal format [field, order] + normalized.sort = normalized.sort.map((item: any) => [ + item.field, + item.order || item.direction || item.dir || 'asc' + ]); + } + } + + return normalized; + } + async find(objectName: string, query: any, options?: any): Promise { - return this.request('find', objectName, query); + const normalizedQuery = this.normalizeQuery(query); + return this.request('find', objectName, normalizedQuery); } async findOne(objectName: string, id: string | number, query?: any, options?: any): Promise { - return this.request('findOne', objectName, { id, query }); // Note: args format must match server expectation + const normalizedQuery = query ? this.normalizeQuery(query) : undefined; + return this.request('findOne', objectName, { id, query: normalizedQuery }); // Note: args format must match server expectation } async create(objectName: string, data: any, options?: any): Promise { diff --git a/packages/drivers/sql/src/index.ts b/packages/drivers/sql/src/index.ts index fc5e14a1..6ed4f2cb 100644 --- a/packages/drivers/sql/src/index.ts +++ b/packages/drivers/sql/src/index.ts @@ -261,14 +261,14 @@ export class SqlDriver implements Driver { } async count(objectName: string, filters: any, options?: any): Promise { - // Normalize the query to support both QueryAST and legacy formats - const normalizedQuery = this.normalizeQuery(filters); const builder = this.getBuilder(objectName, options); - let actualFilters = normalizedQuery; - // If filters is a query object with a 'filters' property, use that - if (normalizedQuery && !Array.isArray(normalizedQuery) && normalizedQuery.filters) { - actualFilters = normalizedQuery.filters; + // Handle both filter arrays and query objects + let actualFilters = filters; + if (filters && !Array.isArray(filters)) { + // It's a query object, normalize it and extract filters + const normalizedQuery = this.normalizeQuery(filters); + actualFilters = normalizedQuery.filters || filters; } if (actualFilters) { diff --git a/packages/foundation/platform-node/jest.config.js b/packages/foundation/platform-node/jest.config.js index 7ba42471..be9fe78b 100644 --- a/packages/foundation/platform-node/jest.config.js +++ b/packages/foundation/platform-node/jest.config.js @@ -12,10 +12,18 @@ module.exports = { testMatch: ['**/test/**/*.test.ts'], moduleNameMapper: { '^@objectql/(.*)$': '/../$1/src', + '^@objectstack/runtime$': '/test/__mocks__/@objectstack/runtime.ts', }, transform: { '^.+\\.ts$': ['ts-jest', { isolatedModules: true, + tsconfig: { + esModuleInterop: true, + allowSyntheticDefaultImports: true, + } }], }, + transformIgnorePatterns: [ + 'node_modules/(?!(@objectstack))', + ], }; diff --git a/packages/foundation/platform-node/test/__mocks__/@objectstack/runtime.ts b/packages/foundation/platform-node/test/__mocks__/@objectstack/runtime.ts new file mode 100644 index 00000000..87120dfd --- /dev/null +++ b/packages/foundation/platform-node/test/__mocks__/@objectstack/runtime.ts @@ -0,0 +1,132 @@ +/** + * Mock for @objectstack/runtime + * This mock is needed because the npm package has issues with Jest + * and we want to focus on testing ObjectQL's logic, not the kernel integration. + * + * For now, this mock delegates to the legacy driver to maintain backward compatibility + * during the migration phase. + */ + +export class ObjectStackKernel { + public ql: unknown = null; + private plugins: any[] = []; + private driver: any = null; // Will be set by the ObjectQL app + + constructor(plugins: any[] = []) { + this.plugins = plugins; + } + + // Method to set the driver for delegation during migration + setDriver(driver: any): void { + this.driver = driver; + } + + async start(): Promise { + // Mock implementation that calls plugin lifecycle methods + for (const plugin of this.plugins) { + if (plugin.install) { + await plugin.install({ engine: this }); + } + } + for (const plugin of this.plugins) { + if (plugin.onStart) { + await plugin.onStart({ engine: this }); + } + } + } + + async seed(): Promise { + // Mock implementation + } + + async find(objectName: string, query: any): Promise<{ value: Record[]; count: number }> { + // Delegate to driver during migration phase + if (this.driver) { + // Convert QueryAST back to UnifiedQuery format for driver + const unifiedQuery: any = {}; + + if (query.fields) { + unifiedQuery.fields = query.fields; + } + if (query.filters) { + unifiedQuery.filters = query.filters; + } + if (query.sort) { + unifiedQuery.sort = query.sort.map((s: any) => [s.field, s.order]); + } + if (query.top !== undefined) { + unifiedQuery.limit = query.top; + } + if (query.skip !== undefined) { + unifiedQuery.skip = query.skip; + } + if (query.aggregations) { + unifiedQuery.aggregate = query.aggregations.map((agg: any) => ({ + func: agg.function, + field: agg.field, + alias: agg.alias + })); + } + if (query.groupBy) { + unifiedQuery.groupBy = query.groupBy; + } + + const results = await this.driver.find(objectName, unifiedQuery, {}); + return { value: results, count: results.length }; + } + return { value: [], count: 0 }; + } + + async get(objectName: string, id: string): Promise> { + // Delegate to driver during migration phase + if (this.driver) { + return await this.driver.findOne(objectName, id, {}, {}); + } + return {}; + } + + async create(objectName: string, data: any): Promise> { + // Delegate to driver during migration phase + if (this.driver) { + return await this.driver.create(objectName, data, {}); + } + return data; + } + + async update(objectName: string, id: string, data: any): Promise> { + // Delegate to driver during migration phase + if (this.driver) { + return await this.driver.update(objectName, id, data, {}); + } + return data; + } + + async delete(objectName: string, id: string): Promise { + // Delegate to driver during migration phase + if (this.driver) { + await this.driver.delete(objectName, id, {}); + return true; + } + return true; + } + + getMetadata(objectName: string): any { + return {}; + } + + getView(objectName: string, viewType?: 'list' | 'form'): any { + return null; + } +} + +export class ObjectStackRuntimeProtocol {} + +export interface RuntimeContext { + engine: ObjectStackKernel; +} + +export interface RuntimePlugin { + name: string; + install?: (ctx: RuntimeContext) => void | Promise; + onStart?: (ctx: RuntimeContext) => void | Promise; +}