@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/MIGRATION.md +451 -0
- package/dist/index.d.ts +107 -1
- package/dist/index.js +272 -12
- package/dist/index.js.map +1 -1
- package/jest.config.js +8 -0
- package/package.json +4 -3
- package/src/index.ts +318 -11
- package/test/index.test.ts +8 -0
- package/tsconfig.tsbuildinfo +1 -1
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 (
|
|
88
|
-
results = this.applyFilters(results,
|
|
156
|
+
if (normalizedQuery.filters) {
|
|
157
|
+
results = this.applyFilters(results, normalizedQuery.filters);
|
|
89
158
|
}
|
|
90
159
|
|
|
91
160
|
// Apply sorting
|
|
92
|
-
if (
|
|
93
|
-
results = this.applySort(results,
|
|
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 (
|
|
98
|
-
results = results.slice(
|
|
166
|
+
if (normalizedQuery.skip) {
|
|
167
|
+
results = results.slice(normalizedQuery.skip);
|
|
99
168
|
}
|
|
100
|
-
if (
|
|
101
|
-
results = results.slice(0,
|
|
169
|
+
if (normalizedQuery.limit) {
|
|
170
|
+
results = results.slice(0, normalizedQuery.limit);
|
|
102
171
|
}
|
|
103
172
|
|
|
104
173
|
// Apply field projection
|
|
105
|
-
if (
|
|
106
|
-
results = results.map(doc => this.projectFields(doc,
|
|
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
|
}
|
package/test/index.test.ts
CHANGED