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.
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
3133import { Driver } from '@objectql/types' ;
34+ import { DriverInterface , QueryAST , FilterNode , SortNode } from '@objectstack/spec' ;
3235import { 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.
0 commit comments