-
Notifications
You must be signed in to change notification settings - Fork 2
Standardize remaining drivers to DriverInterface specification #161
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
108f142
dbc9b64
478b7c6
cf65974
1b19db7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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<string, ExcelJS.Workbook>; // 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<void> { | ||||||||||||||||||||
| await this.init(); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /** | ||||||||||||||||||||
| * Check database connection health | ||||||||||||||||||||
| */ | ||||||||||||||||||||
| async checkHealth(): Promise<boolean> { | ||||||||||||||||||||
| 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<any[]> { | ||||||||||||||||||||
| // 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' | ||||||||||||||||||||
| ]); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
||||||||||||||||||||
| } | |
| } | |
| // Normalize aggregations -> aggregate for aggregation queries | |
| // This aligns the Excel driver with MongoDriver and SqlDriver behavior. | |
| if (normalized.aggregations !== undefined && normalized.aggregate === undefined) { | |
| // Do a simple aliasing to preserve the original aggregation structure. | |
| normalized.aggregate = normalized.aggregations; | |
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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<string, number>; | ||||||||||||||||
| private cache: Map<string, any[]>; | ||||||||||||||||
|
|
@@ -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<void> { | ||||||||||||||||
| // No-op: FileSystem driver doesn't need connection | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| /** | ||||||||||||||||
| * Check database connection health | ||||||||||||||||
| */ | ||||||||||||||||
| async checkHealth(): Promise<boolean> { | ||||||||||||||||
| 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; | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
Comment on lines
+110
to
+122
|
||||||||||||||||
|
|
||||||||||||||||
| /** | ||||||||||||||||
| * 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<any[]> { | ||||||||||||||||
| // 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' | ||||||||||||||||
| ]); | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
||||||||||||||||
| } | |
| } | |
| // Normalize aggregations -> aggregate (for consistency with other drivers) | |
| if (normalized.aggregations !== undefined && normalized.aggregate === undefined) { | |
| normalized.aggregate = normalized.aggregations; | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The checkHealth method uses synchronous file system operations (fs.existsSync) which is inconsistent with the async method signature and could block the event loop. Consider using async fs operations (fs.promises.access or fs.promises.stat) for better performance and consistency with the async pattern.