Skip to content

Commit 1334a87

Browse files
Copilothotlong
andcommitted
feat: add executeQuery and executeCommand to Redis driver v4.0.0
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 06d2638 commit 1334a87

3 files changed

Lines changed: 288 additions & 9 deletions

File tree

packages/drivers/redis/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@objectql/driver-redis",
3-
"version": "3.0.1",
4-
"description": "Redis driver for ObjectQL - Example implementation for key-value storage",
3+
"version": "4.0.0",
4+
"description": "Redis driver for ObjectQL - Example implementation for key-value storage with DriverInterface v4.0 compliance",
55
"keywords": [
66
"objectql",
77
"driver",
@@ -20,6 +20,7 @@
2020
},
2121
"dependencies": {
2222
"@objectql/types": "workspace:*",
23+
"@objectstack/spec": "^0.2.0",
2324
"redis": "^4.6.0"
2425
},
2526
"devDependencies": {

packages/drivers/redis/src/index.ts

Lines changed: 282 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* It adapts Redis (a key-value store) to work with ObjectQL's universal data protocol.
1414
*
1515
* Implements both the legacy Driver interface from @objectql/types and
16-
* the standard DriverInterface from @objectstack/spec for compatibility
16+
* the standard DriverInterface from @objectstack/spec for full compatibility
1717
* with the new kernel-based plugin system.
1818
*
1919
* ⚠️ WARNING: This is an educational example, not production-ready.
@@ -26,11 +26,38 @@
2626
*
2727
* Note: This example implements only the core required methods from the Driver interface.
2828
* Optional methods like introspectSchema(), aggregate(), transactions, etc. are not implemented.
29+
*
30+
* @version 4.0.0 - DriverInterface compliant
2931
*/
3032

3133
import { Driver } from '@objectql/types';
34+
import { DriverInterface, QueryAST, FilterNode, SortNode } from '@objectstack/spec';
3235
import { createClient, RedisClientType } from 'redis';
3336

37+
/**
38+
* Command interface for executeCommand method
39+
*/
40+
export interface Command {
41+
type: 'create' | 'update' | 'delete' | 'bulkCreate' | 'bulkUpdate' | 'bulkDelete';
42+
object: string;
43+
data?: any;
44+
id?: string | number;
45+
ids?: Array<string | number>;
46+
records?: any[];
47+
updates?: Array<{id: string | number, data: any}>;
48+
options?: any;
49+
}
50+
51+
/**
52+
* Command result interface
53+
*/
54+
export interface CommandResult {
55+
success: boolean;
56+
data?: any;
57+
affected: number;
58+
error?: string;
59+
}
60+
3461
/**
3562
* Configuration options for the Redis driver.
3663
*/
@@ -49,10 +76,10 @@ export interface RedisDriverConfig {
4976
*
5077
* Example: `users:user-123` → `{"id":"user-123","name":"Alice",...}`
5178
*/
52-
export class RedisDriver implements Driver {
79+
export class RedisDriver implements Driver, DriverInterface {
5380
// Driver metadata (ObjectStack-compatible)
5481
public readonly name = 'RedisDriver';
55-
public readonly version = '3.0.1';
82+
public readonly version = '4.0.0';
5683
public readonly supports = {
5784
transactions: false,
5885
joins: false,
@@ -161,7 +188,7 @@ export class RedisDriver implements Driver {
161188

162189
// If ID is provided, fetch directly
163190
if (id) {
164-
const key = `${objectName}:${id}`;
191+
const key = this.generateRedisKey(objectName, id);
165192
const data = await this.client.get(key);
166193

167194
if (!data) {
@@ -202,7 +229,7 @@ export class RedisDriver implements Driver {
202229
updated_at: data.updated_at || now
203230
};
204231

205-
const key = `${objectName}:${id}`;
232+
const key = this.generateRedisKey(objectName, id);
206233
await this.client.set(key, JSON.stringify(doc));
207234

208235
return doc;
@@ -214,7 +241,7 @@ export class RedisDriver implements Driver {
214241
async update(objectName: string, id: string | number, data: any, options?: any): Promise<any> {
215242
await this.connected;
216243

217-
const key = `${objectName}:${id}`;
244+
const key = this.generateRedisKey(objectName, id);
218245
const existing = await this.client.get(key);
219246

220247
if (!existing) {
@@ -241,7 +268,7 @@ export class RedisDriver implements Driver {
241268
async delete(objectName: string, id: string | number, options?: any): Promise<any> {
242269
await this.connected;
243270

244-
const key = `${objectName}:${id}`;
271+
const key = this.generateRedisKey(objectName, id);
245272
const result = await this.client.del(key);
246273

247274
return result > 0;
@@ -295,8 +322,256 @@ export class RedisDriver implements Driver {
295322
await this.client.quit();
296323
}
297324

325+
/**
326+
* Execute a query (DriverInterface v4.0 method)
327+
*
328+
* This method handles all read operations using the QueryAST format.
329+
* Converts QueryAST to legacy query format and delegates to find().
330+
*
331+
* @param ast - The query AST
332+
* @param options - Optional execution options
333+
* @returns Query results with count
334+
*/
335+
async executeQuery(ast: QueryAST, options?: any): Promise<{ value: any[]; count?: number }> {
336+
const objectName = ast.object || '';
337+
338+
// Convert QueryAST to legacy query format
339+
const legacyQuery: any = {
340+
fields: ast.fields,
341+
filters: this.convertFilterNodeToLegacy(ast.filters),
342+
sort: ast.sort?.map((s: SortNode) => [s.field, s.order]),
343+
limit: ast.top,
344+
skip: ast.skip,
345+
};
346+
347+
// Use existing find method
348+
const results = await this.find(objectName, legacyQuery, options);
349+
350+
return {
351+
value: results,
352+
count: results.length
353+
};
354+
}
355+
356+
/**
357+
* Execute a command (DriverInterface v4.0 method)
358+
*
359+
* This method handles all mutation operations (create, update, delete)
360+
* using a unified command interface.
361+
*
362+
* Supports both single operations and bulk operations using Redis PIPELINE
363+
* for optimal performance.
364+
*
365+
* @param command - The command to execute
366+
* @param parameters - Optional command parameters (unused in this driver)
367+
* @param options - Optional execution options
368+
* @returns Command execution result
369+
*/
370+
async executeCommand(command: Command, options?: any): Promise<CommandResult> {
371+
try {
372+
await this.connected;
373+
const cmdOptions = { ...options, ...command.options };
374+
375+
switch (command.type) {
376+
case 'create':
377+
if (!command.data) {
378+
throw new Error('Create command requires data');
379+
}
380+
const created = await this.create(command.object, command.data, cmdOptions);
381+
return {
382+
success: true,
383+
data: created,
384+
affected: 1
385+
};
386+
387+
case 'update':
388+
if (!command.id || !command.data) {
389+
throw new Error('Update command requires id and data');
390+
}
391+
const updated = await this.update(command.object, command.id, command.data, cmdOptions);
392+
return {
393+
success: true,
394+
data: updated,
395+
affected: 1
396+
};
397+
398+
case 'delete':
399+
if (!command.id) {
400+
throw new Error('Delete command requires id');
401+
}
402+
await this.delete(command.object, command.id, cmdOptions);
403+
return {
404+
success: true,
405+
affected: 1
406+
};
407+
408+
case 'bulkCreate':
409+
if (!command.records || !Array.isArray(command.records)) {
410+
throw new Error('BulkCreate command requires records array');
411+
}
412+
// Use Redis PIPELINE for batch operations
413+
const pipeline = this.client.multi();
414+
const bulkCreated: any[] = [];
415+
const now = new Date().toISOString();
416+
417+
for (const record of command.records) {
418+
const id = record.id || this.generateId();
419+
const doc = {
420+
...record,
421+
id,
422+
created_at: record.created_at || now,
423+
updated_at: record.updated_at || now
424+
};
425+
bulkCreated.push(doc);
426+
const key = this.generateRedisKey(command.object, id);
427+
pipeline.set(key, JSON.stringify(doc));
428+
}
429+
430+
await pipeline.exec();
431+
432+
return {
433+
success: true,
434+
data: bulkCreated,
435+
affected: command.records.length
436+
};
437+
438+
case 'bulkUpdate':
439+
if (!command.updates || !Array.isArray(command.updates)) {
440+
throw new Error('BulkUpdate command requires updates array');
441+
}
442+
// Use Redis PIPELINE for batch operations
443+
const updatePipeline = this.client.multi();
444+
const updateResults: any[] = [];
445+
const updateTime = new Date().toISOString();
446+
447+
for (const update of command.updates) {
448+
const key = this.generateRedisKey(command.object, update.id);
449+
const existing = await this.client.get(key);
450+
451+
if (existing) {
452+
const existingDoc = JSON.parse(existing);
453+
const doc = {
454+
...existingDoc,
455+
...update.data,
456+
id: update.id,
457+
created_at: existingDoc.created_at,
458+
updated_at: updateTime
459+
};
460+
updateResults.push(doc);
461+
updatePipeline.set(key, JSON.stringify(doc));
462+
}
463+
}
464+
465+
await updatePipeline.exec();
466+
467+
return {
468+
success: true,
469+
data: updateResults,
470+
affected: updateResults.length
471+
};
472+
473+
case 'bulkDelete':
474+
if (!command.ids || !Array.isArray(command.ids)) {
475+
throw new Error('BulkDelete command requires ids array');
476+
}
477+
// Use Redis PIPELINE for batch operations
478+
const deletePipeline = this.client.multi();
479+
480+
for (const id of command.ids) {
481+
const key = this.generateRedisKey(command.object, id);
482+
deletePipeline.del(key);
483+
}
484+
485+
const deleteResults = await deletePipeline.exec();
486+
const deleted = deleteResults?.filter((r: any) => r && r[1] > 0).length || 0;
487+
488+
return {
489+
success: true,
490+
affected: deleted
491+
};
492+
493+
default:
494+
throw new Error(`Unknown command type: ${(command as any).type}`);
495+
}
496+
} catch (error: any) {
497+
return {
498+
success: false,
499+
error: error.message || 'Command execution failed',
500+
affected: 0
501+
};
502+
}
503+
}
504+
298505
// ========== Helper Methods ==========
299506

507+
/**
508+
* Convert FilterNode (QueryAST format) to legacy filter array format
509+
* This allows reuse of existing filter logic while supporting new QueryAST
510+
*
511+
* @private
512+
*/
513+
private convertFilterNodeToLegacy(node?: FilterNode): any {
514+
if (!node) return undefined;
515+
516+
switch (node.type) {
517+
case 'comparison':
518+
// Convert comparison node to [field, operator, value] format
519+
const operator = node.operator || '=';
520+
return [[node.field, operator, node.value]];
521+
522+
case 'and':
523+
// Convert AND node to array with 'and' separator
524+
if (!node.children || node.children.length === 0) return undefined;
525+
const andResults: any[] = [];
526+
for (const child of node.children) {
527+
const converted = this.convertFilterNodeToLegacy(child);
528+
if (converted) {
529+
if (andResults.length > 0) {
530+
andResults.push('and');
531+
}
532+
andResults.push(...(Array.isArray(converted) ? converted : [converted]));
533+
}
534+
}
535+
return andResults.length > 0 ? andResults : undefined;
536+
537+
case 'or':
538+
// Convert OR node to array with 'or' separator
539+
if (!node.children || node.children.length === 0) return undefined;
540+
const orResults: any[] = [];
541+
for (const child of node.children) {
542+
const converted = this.convertFilterNodeToLegacy(child);
543+
if (converted) {
544+
if (orResults.length > 0) {
545+
orResults.push('or');
546+
}
547+
orResults.push(...(Array.isArray(converted) ? converted : [converted]));
548+
}
549+
}
550+
return orResults.length > 0 ? orResults : undefined;
551+
552+
case 'not':
553+
// NOT is not directly supported in legacy format
554+
// We could implement it by negating the child operators
555+
console.warn('[RedisDriver] NOT operator in filters is not fully supported in legacy format');
556+
return undefined;
557+
558+
default:
559+
return undefined;
560+
}
561+
}
562+
563+
/**
564+
* Generate Redis key for an object record
565+
*
566+
* Strategy: objectName:id
567+
* Example: users:user-123
568+
*
569+
* @private
570+
*/
571+
private generateRedisKey(objectName: string, id: string | number): string {
572+
return `${objectName}:${id}`;
573+
}
574+
300575
/**
301576
* Normalizes query format to support both legacy UnifiedQuery and QueryAST formats.
302577
* This ensures backward compatibility while supporting the new @objectstack/spec interface.

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)