@objectql/driver-memory 3.0.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -1,9 +1,21 @@
1
+ /**
2
+ * ObjectQL
3
+ * Copyright (c) 2026-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
1
9
  /**
2
10
  * Memory Driver for ObjectQL (Production-Ready)
3
11
  *
4
12
  * A high-performance in-memory driver for ObjectQL that stores data in JavaScript Maps.
5
13
  * Perfect for testing, development, and environments where persistence is not required.
6
14
  *
15
+ * Implements both the legacy Driver interface from @objectql/types and
16
+ * the standard DriverInterface from @objectstack/spec for full compatibility
17
+ * with the new kernel-based plugin system.
18
+ *
7
19
  * ✅ Production-ready features:
8
20
  * - Zero external dependencies
9
21
  * - Thread-safe operations
@@ -17,9 +29,36 @@
17
29
  * - Edge/Worker environments (Cloudflare Workers, Deno Deploy)
18
30
  * - Client-side state management
19
31
  * - Temporary data caching
32
+ *
33
+ * @version 4.0.0 - DriverInterface compliant
20
34
  */
21
35
 
22
36
  import { Driver, ObjectQLError } from '@objectql/types';
37
+ import { DriverInterface, QueryAST, FilterNode, SortNode } from '@objectstack/spec';
38
+
39
+ /**
40
+ * Command interface for executeCommand method
41
+ */
42
+ export interface Command {
43
+ type: 'create' | 'update' | 'delete' | 'bulkCreate' | 'bulkUpdate' | 'bulkDelete';
44
+ object: string;
45
+ data?: any;
46
+ id?: string | number;
47
+ ids?: Array<string | number>;
48
+ records?: any[];
49
+ updates?: Array<{id: string | number, data: any}>;
50
+ options?: any;
51
+ }
52
+
53
+ /**
54
+ * Command result interface
55
+ */
56
+ export interface CommandResult {
57
+ success: boolean;
58
+ data?: any;
59
+ affected: number; // Required (changed from optional)
60
+ error?: string;
61
+ }
23
62
 
24
63
  /**
25
64
  * Configuration options for the Memory driver.
@@ -39,7 +78,18 @@ export interface MemoryDriverConfig {
39
78
  *
40
79
  * Example: `users:user-123` → `{id: "user-123", name: "Alice", ...}`
41
80
  */
42
- export class MemoryDriver implements Driver {
81
+ export class MemoryDriver implements Driver, DriverInterface {
82
+ // Driver metadata (ObjectStack-compatible)
83
+ public readonly name = 'MemoryDriver';
84
+ public readonly version = '4.0.0';
85
+ public readonly supports = {
86
+ transactions: false,
87
+ joins: false,
88
+ fullTextSearch: false,
89
+ jsonFields: true,
90
+ arrayFields: true
91
+ };
92
+
43
93
  private store: Map<string, any>;
44
94
  private config: MemoryDriverConfig;
45
95
  private idCounters: Map<string, number>;
@@ -55,6 +105,22 @@ export class MemoryDriver implements Driver {
55
105
  }
56
106
  }
57
107
 
108
+ /**
109
+ * Connect to the database (for DriverInterface compatibility)
110
+ * This is a no-op for memory driver as there's no external connection.
111
+ */
112
+ async connect(): Promise<void> {
113
+ // No-op: Memory driver doesn't need connection
114
+ }
115
+
116
+ /**
117
+ * Check database connection health
118
+ */
119
+ async checkHealth(): Promise<boolean> {
120
+ // Memory driver is always healthy if it exists
121
+ return true;
122
+ }
123
+
58
124
  /**
59
125
  * Load initial data into the store.
60
126
  */
@@ -73,6 +139,9 @@ export class MemoryDriver implements Driver {
73
139
  * Supports filtering, sorting, pagination, and field projection.
74
140
  */
75
141
  async find(objectName: string, query: any = {}, options?: any): Promise<any[]> {
142
+ // Normalize query to support both legacy and QueryAST formats
143
+ const normalizedQuery = this.normalizeQuery(query);
144
+
76
145
  // Get all records for this object type
77
146
  const pattern = `${objectName}:`;
78
147
  let results: any[] = [];
@@ -84,26 +153,26 @@ export class MemoryDriver implements Driver {
84
153
  }
85
154
 
86
155
  // Apply filters
87
- if (query.filters) {
88
- results = this.applyFilters(results, query.filters);
156
+ if (normalizedQuery.filters) {
157
+ results = this.applyFilters(results, normalizedQuery.filters);
89
158
  }
90
159
 
91
160
  // Apply sorting
92
- if (query.sort && Array.isArray(query.sort)) {
93
- results = this.applySort(results, query.sort);
161
+ if (normalizedQuery.sort && Array.isArray(normalizedQuery.sort)) {
162
+ results = this.applySort(results, normalizedQuery.sort);
94
163
  }
95
164
 
96
165
  // Apply pagination
97
- if (query.skip) {
98
- results = results.slice(query.skip);
166
+ if (normalizedQuery.skip) {
167
+ results = results.slice(normalizedQuery.skip);
99
168
  }
100
- if (query.limit) {
101
- results = results.slice(0, query.limit);
169
+ if (normalizedQuery.limit) {
170
+ results = results.slice(0, normalizedQuery.limit);
102
171
  }
103
172
 
104
173
  // Apply field projection
105
- if (query.fields && Array.isArray(query.fields)) {
106
- results = results.map(doc => this.projectFields(doc, query.fields));
174
+ if (normalizedQuery.fields && Array.isArray(normalizedQuery.fields)) {
175
+ results = results.map(doc => this.projectFields(doc, normalizedQuery.fields));
107
176
  }
108
177
 
109
178
  return results;
@@ -346,6 +415,39 @@ export class MemoryDriver implements Driver {
346
415
 
347
416
  // ========== Helper Methods ==========
348
417
 
418
+ /**
419
+ * Normalizes query format to support both legacy UnifiedQuery and QueryAST formats.
420
+ * This ensures backward compatibility while supporting the new @objectstack/spec interface.
421
+ *
422
+ * QueryAST format uses 'top' for limit, while UnifiedQuery uses 'limit'.
423
+ * QueryAST sort is array of {field, order}, while UnifiedQuery is array of [field, order].
424
+ */
425
+ private normalizeQuery(query: any): any {
426
+ if (!query) return {};
427
+
428
+ const normalized: any = { ...query };
429
+
430
+ // Normalize limit/top
431
+ if (normalized.top !== undefined && normalized.limit === undefined) {
432
+ normalized.limit = normalized.top;
433
+ }
434
+
435
+ // Normalize sort format
436
+ if (normalized.sort && Array.isArray(normalized.sort)) {
437
+ // Check if it's already in the array format [field, order]
438
+ const firstSort = normalized.sort[0];
439
+ if (firstSort && typeof firstSort === 'object' && !Array.isArray(firstSort)) {
440
+ // Convert from QueryAST format {field, order} to internal format [field, order]
441
+ normalized.sort = normalized.sort.map((item: any) => [
442
+ item.field,
443
+ item.order || item.direction || item.dir || 'asc'
444
+ ]);
445
+ }
446
+ }
447
+
448
+ return normalized;
449
+ }
450
+
349
451
  /**
350
452
  * Apply filters to an array of records (in-memory filtering).
351
453
  *
@@ -524,4 +626,209 @@ export class MemoryDriver implements Driver {
524
626
  const timestamp = Date.now();
525
627
  return `${objectName}-${timestamp}-${counter}`;
526
628
  }
629
+
630
+ /**
631
+ * Execute a query using QueryAST (DriverInterface v4.0 method)
632
+ *
633
+ * This is the new standard method for query execution using the
634
+ * ObjectStack QueryAST format.
635
+ *
636
+ * @param ast - The QueryAST representing the query
637
+ * @param options - Optional execution options
638
+ * @returns Query results with value and count
639
+ */
640
+ async executeQuery(ast: QueryAST, options?: any): Promise<{ value: any[]; count?: number }> {
641
+ const objectName = ast.object || '';
642
+
643
+ // Convert QueryAST to legacy query format
644
+ const legacyQuery: any = {
645
+ fields: ast.fields,
646
+ filters: this.convertFilterNodeToLegacy(ast.filters),
647
+ sort: ast.sort?.map((s: SortNode) => [s.field, s.order]),
648
+ limit: ast.top,
649
+ offset: ast.skip,
650
+ };
651
+
652
+ // Use existing find method
653
+ const results = await this.find(objectName, legacyQuery, options);
654
+
655
+ return {
656
+ value: results,
657
+ count: results.length
658
+ };
659
+ }
660
+
661
+ /**
662
+ * Execute a command (DriverInterface v4.0 method)
663
+ *
664
+ * This method handles all mutation operations (create, update, delete)
665
+ * using a unified command interface.
666
+ *
667
+ * @param command - The command to execute
668
+ * @param parameters - Optional command parameters (unused in this driver)
669
+ * @param options - Optional execution options
670
+ * @returns Command execution result
671
+ */
672
+ async executeCommand(command: Command, options?: any): Promise<CommandResult> {
673
+ try {
674
+ const cmdOptions = { ...options, ...command.options };
675
+
676
+ switch (command.type) {
677
+ case 'create':
678
+ if (!command.data) {
679
+ throw new Error('Create command requires data');
680
+ }
681
+ const created = await this.create(command.object, command.data, cmdOptions);
682
+ return {
683
+ success: true,
684
+ data: created,
685
+ affected: 1
686
+ };
687
+
688
+ case 'update':
689
+ if (!command.id || !command.data) {
690
+ throw new Error('Update command requires id and data');
691
+ }
692
+ const updated = await this.update(command.object, command.id, command.data, cmdOptions);
693
+ return {
694
+ success: true,
695
+ data: updated,
696
+ affected: 1
697
+ };
698
+
699
+ case 'delete':
700
+ if (!command.id) {
701
+ throw new Error('Delete command requires id');
702
+ }
703
+ await this.delete(command.object, command.id, cmdOptions);
704
+ return {
705
+ success: true,
706
+ affected: 1
707
+ };
708
+
709
+ case 'bulkCreate':
710
+ if (!command.records || !Array.isArray(command.records)) {
711
+ throw new Error('BulkCreate command requires records array');
712
+ }
713
+ const bulkCreated = [];
714
+ for (const record of command.records) {
715
+ const created = await this.create(command.object, record, cmdOptions);
716
+ bulkCreated.push(created);
717
+ }
718
+ return {
719
+ success: true,
720
+ data: bulkCreated,
721
+ affected: command.records.length
722
+ };
723
+
724
+ case 'bulkUpdate':
725
+ if (!command.updates || !Array.isArray(command.updates)) {
726
+ throw new Error('BulkUpdate command requires updates array');
727
+ }
728
+ const updateResults = [];
729
+ for (const update of command.updates) {
730
+ const result = await this.update(command.object, update.id, update.data, cmdOptions);
731
+ updateResults.push(result);
732
+ }
733
+ return {
734
+ success: true,
735
+ data: updateResults,
736
+ affected: command.updates.length
737
+ };
738
+
739
+ case 'bulkDelete':
740
+ if (!command.ids || !Array.isArray(command.ids)) {
741
+ throw new Error('BulkDelete command requires ids array');
742
+ }
743
+ let deleted = 0;
744
+ for (const id of command.ids) {
745
+ const result = await this.delete(command.object, id, cmdOptions);
746
+ if (result) deleted++;
747
+ }
748
+ return {
749
+ success: true,
750
+ affected: deleted
751
+ };
752
+
753
+ default:
754
+ throw new Error(`Unknown command type: ${(command as any).type}`);
755
+ }
756
+ } catch (error: any) {
757
+ return {
758
+ success: false,
759
+ error: error.message || 'Command execution failed',
760
+ affected: 0
761
+ };
762
+ }
763
+ }
764
+
765
+ /**
766
+ * Convert FilterNode (QueryAST format) to legacy filter array format
767
+ * This allows reuse of existing filter logic while supporting new QueryAST
768
+ *
769
+ * @private
770
+ */
771
+ private convertFilterNodeToLegacy(node?: FilterNode): any {
772
+ if (!node) return undefined;
773
+
774
+ switch (node.type) {
775
+ case 'comparison':
776
+ // Convert comparison node to [field, operator, value] format
777
+ const operator = node.operator || '=';
778
+ return [[node.field, operator, node.value]];
779
+
780
+ case 'and':
781
+ // Convert AND node to array with 'and' separator
782
+ if (!node.children || node.children.length === 0) return undefined;
783
+ const andResults: any[] = [];
784
+ for (const child of node.children) {
785
+ const converted = this.convertFilterNodeToLegacy(child);
786
+ if (converted) {
787
+ if (andResults.length > 0) {
788
+ andResults.push('and');
789
+ }
790
+ andResults.push(...(Array.isArray(converted) ? converted : [converted]));
791
+ }
792
+ }
793
+ return andResults.length > 0 ? andResults : undefined;
794
+
795
+ case 'or':
796
+ // Convert OR node to array with 'or' separator
797
+ if (!node.children || node.children.length === 0) return undefined;
798
+ const orResults: any[] = [];
799
+ for (const child of node.children) {
800
+ const converted = this.convertFilterNodeToLegacy(child);
801
+ if (converted) {
802
+ if (orResults.length > 0) {
803
+ orResults.push('or');
804
+ }
805
+ orResults.push(...(Array.isArray(converted) ? converted : [converted]));
806
+ }
807
+ }
808
+ return orResults.length > 0 ? orResults : undefined;
809
+
810
+ case 'not':
811
+ // NOT is complex - we'll just process the first child for now
812
+ if (node.children && node.children.length > 0) {
813
+ return this.convertFilterNodeToLegacy(node.children[0]);
814
+ }
815
+ return undefined;
816
+
817
+ default:
818
+ return undefined;
819
+ }
820
+ }
821
+
822
+ /**
823
+ * Execute command (alternative signature for compatibility)
824
+ *
825
+ * @param command - Command string or object
826
+ * @param parameters - Command parameters
827
+ * @param options - Execution options
828
+ */
829
+ async execute(command: any, parameters?: any[], options?: any): Promise<any> {
830
+ // For memory driver, this is primarily for compatibility
831
+ // We don't support raw SQL/commands
832
+ throw new Error('Memory driver does not support raw command execution. Use executeCommand() instead.');
833
+ }
527
834
  }
@@ -1,3 +1,11 @@
1
+ /**
2
+ * ObjectQL
3
+ * Copyright (c) 2026-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
1
9
  /**
2
10
  * Memory Driver Tests
3
11
  *