Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 100 additions & 10 deletions packages/drivers/excel/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
}
Comment on lines +143 to +170
Copy link

Copilot AI Jan 22, 2026

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.

Copilot uses AI. Check for mistakes.

/**
* Factory method to create and initialize the driver.
*/
Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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'
]);
}
}
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The normalizeQuery implementation is incomplete compared to MongoDriver and SqlDriver. These reference drivers also handle aggregation normalization (converting 'aggregations' to 'aggregate' format), which is missing in this implementation. For consistency with the reference drivers, consider adding aggregation normalization support.

Suggested change
}
}
// 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;
}

Copilot uses AI. Check for mistakes.

return normalized;
}

/**
* Apply filters to an array of records (in-memory filtering).
*/
Expand Down
95 changes: 85 additions & 10 deletions packages/drivers/fs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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[]>;
Expand All @@ -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
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The checkHealth method also uses synchronous file system operations (fs.existsSync, fs.readdirSync) 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.readdir) for better performance and consistency with the async pattern.

Copilot uses AI. Check for mistakes.

/**
* Load initial data into the store.
*/
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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'
]);
}
}
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The normalizeQuery implementation is incomplete compared to MongoDriver and SqlDriver. These reference drivers also handle aggregation normalization (converting 'aggregations' to 'aggregate' format), which is missing in this implementation. For consistency with the reference drivers, consider adding aggregation normalization support.

Suggested change
}
}
// Normalize aggregations -> aggregate (for consistency with other drivers)
if (normalized.aggregations !== undefined && normalized.aggregate === undefined) {
normalized.aggregate = normalized.aggregations;
}

Copilot uses AI. Check for mistakes.

return normalized;
}

/**
* Apply filters to an array of records.
*
Expand Down
Loading
Loading