@objectstack/driver-memory 0.9.2 โ 1.0.1
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/CHANGELOG.md +22 -0
- package/README.md +7 -221
- package/dist/memory-driver.d.ts +9 -0
- package/dist/memory-driver.d.ts.map +1 -1
- package/dist/memory-driver.js +185 -8
- package/dist/memory-driver.test.d.ts +2 -0
- package/dist/memory-driver.test.d.ts.map +1 -0
- package/dist/memory-driver.test.js +93 -0
- package/dist/memory-matcher.d.ts +19 -0
- package/dist/memory-matcher.d.ts.map +1 -0
- package/dist/memory-matcher.js +160 -0
- package/package.json +9 -5
- package/src/memory-driver.test.ts +120 -0
- package/src/memory-driver.ts +213 -9
- package/src/memory-matcher.ts +167 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# @objectstack/driver-memory
|
|
2
2
|
|
|
3
|
+
## 1.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- @objectstack/spec@1.0.1
|
|
8
|
+
- @objectstack/core@1.0.1
|
|
9
|
+
|
|
10
|
+
## 1.0.0
|
|
11
|
+
|
|
12
|
+
### Major Changes
|
|
13
|
+
|
|
14
|
+
- Major version release for ObjectStack Protocol v1.0.
|
|
15
|
+
- Stabilized Protocol Definitions
|
|
16
|
+
- Enhanced Runtime Plugin Support
|
|
17
|
+
- Fixed Type Compliance across Monorepo
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- Updated dependencies
|
|
22
|
+
- @objectstack/spec@1.0.0
|
|
23
|
+
- @objectstack/core@1.0.0
|
|
24
|
+
|
|
3
25
|
## 0.9.2
|
|
4
26
|
|
|
5
27
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -1,235 +1,21 @@
|
|
|
1
1
|
# @objectstack/driver-memory
|
|
2
2
|
|
|
3
|
-
In-Memory
|
|
4
|
-
|
|
5
|
-
## ๐ค AI Development Context
|
|
6
|
-
|
|
7
|
-
**Role**: Reference Driver Implementation
|
|
8
|
-
**Usage**:
|
|
9
|
-
- Use for testing or prototypes.
|
|
10
|
-
- Stores data in JS RAM (volatile).
|
|
11
|
-
- **Do not** use for production.
|
|
12
|
-
|
|
13
|
-
## Plugin Capabilities
|
|
14
|
-
|
|
15
|
-
This driver implements the ObjectStack plugin capability protocol:
|
|
16
|
-
- **Type**: `driver`
|
|
17
|
-
- **Protocol**: `com.objectstack.protocol.storage.v1` (partial conformance)
|
|
18
|
-
- **Provides**: `DriverInterface` for data storage operations
|
|
19
|
-
- **Features**:
|
|
20
|
-
- โ
Basic CRUD operations
|
|
21
|
-
- โ
Pagination (limit/offset)
|
|
22
|
-
- โ Advanced query filters
|
|
23
|
-
- โ Aggregations
|
|
24
|
-
- โ Sorting
|
|
25
|
-
- โ Transactions
|
|
26
|
-
- โ Joins
|
|
27
|
-
|
|
28
|
-
See [objectstack.config.ts](./objectstack.config.ts) for the complete capability manifest.
|
|
3
|
+
In-Memory Database access layer for ObjectStack. Supports rich querying capabilities (MongoDB-style operators) on standard JavaScript arrays.
|
|
29
4
|
|
|
30
5
|
## Features
|
|
31
6
|
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
- โก **Fast**: In-memory operations are lightning fast
|
|
37
|
-
|
|
38
|
-
## Installation
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
pnpm add @objectstack/driver-memory
|
|
42
|
-
```
|
|
7
|
+
- **NoSQL Syntax**: Supports `$eq`, `$gt`, `$lt`, `$in`, `$and`, `$or` operators.
|
|
8
|
+
- **Sorting & Pagination**: Full support for `sort`, `skip`, `limit`.
|
|
9
|
+
- **Zero Config**: Perfect for prototyping, testing, and the **MSW Browser Mock**.
|
|
10
|
+
- **Stateful**: Preserves data in memory during the session.
|
|
43
11
|
|
|
44
12
|
## Usage
|
|
45
13
|
|
|
46
|
-
### With ObjectStack Runtime
|
|
47
|
-
|
|
48
|
-
```typescript
|
|
49
|
-
import { InMemoryDriver } from '@objectstack/driver-memory';
|
|
50
|
-
import { DriverPlugin } from '@objectstack/runtime';
|
|
51
|
-
import { ObjectKernel } from '@objectstack/runtime';
|
|
52
|
-
|
|
53
|
-
const kernel = new ObjectKernel();
|
|
54
|
-
|
|
55
|
-
// Create and register the driver
|
|
56
|
-
const memoryDriver = new InMemoryDriver();
|
|
57
|
-
kernel.use(new DriverPlugin(memoryDriver, 'memory'));
|
|
58
|
-
|
|
59
|
-
await kernel.bootstrap();
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
### Standalone Usage
|
|
63
|
-
|
|
64
14
|
```typescript
|
|
65
15
|
import { InMemoryDriver } from '@objectstack/driver-memory';
|
|
66
16
|
|
|
67
|
-
const driver = new InMemoryDriver(
|
|
68
|
-
seedData: true // Pre-populate with example data
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
// Initialize
|
|
17
|
+
const driver = new InMemoryDriver();
|
|
72
18
|
await driver.connect();
|
|
73
19
|
|
|
74
|
-
//
|
|
75
|
-
const user = await driver.create('user', {
|
|
76
|
-
name: 'John Doe',
|
|
77
|
-
email: 'john@example.com'
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
// Find records
|
|
81
|
-
const users = await driver.find('user', {
|
|
82
|
-
limit: 10,
|
|
83
|
-
offset: 0
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
// Get by ID
|
|
87
|
-
const foundUser = await driver.findOne('user', user.id);
|
|
88
|
-
|
|
89
|
-
// Update
|
|
90
|
-
await driver.update('user', user.id, {
|
|
91
|
-
name: 'Jane Doe'
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
// Delete
|
|
95
|
-
await driver.delete('user', user.id);
|
|
96
|
-
|
|
97
|
-
// Count
|
|
98
|
-
const count = await driver.count('user');
|
|
99
|
-
|
|
100
|
-
// Cleanup
|
|
101
|
-
await driver.disconnect();
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
## API Reference
|
|
105
|
-
|
|
106
|
-
### InMemoryDriver
|
|
107
|
-
|
|
108
|
-
The main driver class that implements `DriverInterface`.
|
|
109
|
-
|
|
110
|
-
#### Constructor Options
|
|
111
|
-
|
|
112
|
-
```typescript
|
|
113
|
-
interface DriverOptions {
|
|
114
|
-
/**
|
|
115
|
-
* Pre-populate the database with example data on startup
|
|
116
|
-
* @default false
|
|
117
|
-
*/
|
|
118
|
-
seedData?: boolean;
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Logger instance
|
|
122
|
-
*/
|
|
123
|
-
logger?: Logger;
|
|
124
|
-
}
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
#### Methods
|
|
128
|
-
|
|
129
|
-
- `connect()` - Initialize the driver (no-op for in-memory)
|
|
130
|
-
- `disconnect()` - Cleanup resources (clears all data)
|
|
131
|
-
- `create(object, data)` - Create a new record
|
|
132
|
-
- `find(object, query?)` - Query records with optional pagination
|
|
133
|
-
- `findOne(object, id)` - Get a single record by ID
|
|
134
|
-
- `update(object, id, data)` - Update a record
|
|
135
|
-
- `delete(object, id)` - Delete a record
|
|
136
|
-
- `count(object, query?)` - Count total records
|
|
137
|
-
- `getSchema(object)` - Get object schema definition
|
|
138
|
-
- `query(query)` - Execute a raw query (limited support)
|
|
139
|
-
|
|
140
|
-
#### Capabilities
|
|
141
|
-
|
|
142
|
-
The driver declares its capabilities via the `supports` property:
|
|
143
|
-
|
|
144
|
-
```typescript
|
|
145
|
-
{
|
|
146
|
-
transactions: false,
|
|
147
|
-
queryFilters: false,
|
|
148
|
-
queryAggregations: false,
|
|
149
|
-
querySorting: false,
|
|
150
|
-
queryPagination: true, // โ
Supported via limit/offset
|
|
151
|
-
queryWindowFunctions: false,
|
|
152
|
-
querySubqueries: false,
|
|
153
|
-
joins: false,
|
|
154
|
-
fullTextSearch: false,
|
|
155
|
-
vectorSearch: false,
|
|
156
|
-
geoSpatial: false
|
|
157
|
-
}
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
## Data Storage
|
|
161
|
-
|
|
162
|
-
The in-memory driver stores data in a simple Map structure:
|
|
163
|
-
|
|
164
|
-
```typescript
|
|
165
|
-
private tables: Map<string, Array<Record<string, any>>> = new Map();
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
**Important**: All data is lost when the process exits or `disconnect()` is called. This driver is **not suitable for production use**.
|
|
169
|
-
|
|
170
|
-
## Use Cases
|
|
171
|
-
|
|
172
|
-
โ
**Good for:**
|
|
173
|
-
- Unit testing
|
|
174
|
-
- Integration testing
|
|
175
|
-
- Development/prototyping
|
|
176
|
-
- CI/CD pipelines
|
|
177
|
-
- Examples and tutorials
|
|
178
|
-
- Learning ObjectStack
|
|
179
|
-
|
|
180
|
-
โ **Not suitable for:**
|
|
181
|
-
- Production environments
|
|
182
|
-
- Data persistence requirements
|
|
183
|
-
- Large datasets (memory constraints)
|
|
184
|
-
- Multi-process scenarios
|
|
185
|
-
- Concurrent write operations
|
|
186
|
-
|
|
187
|
-
## Testing Example
|
|
188
|
-
|
|
189
|
-
```typescript
|
|
190
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
191
|
-
import { InMemoryDriver } from '@objectstack/driver-memory';
|
|
192
|
-
|
|
193
|
-
describe('User CRUD', () => {
|
|
194
|
-
let driver: InMemoryDriver;
|
|
195
|
-
|
|
196
|
-
beforeEach(async () => {
|
|
197
|
-
driver = new InMemoryDriver();
|
|
198
|
-
await driver.connect();
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
afterEach(async () => {
|
|
202
|
-
await driver.disconnect();
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it('should create and retrieve a user', async () => {
|
|
206
|
-
const user = await driver.create('user', {
|
|
207
|
-
name: 'Test User',
|
|
208
|
-
email: 'test@example.com'
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
expect(user.id).toBeDefined();
|
|
212
|
-
|
|
213
|
-
const found = await driver.findOne('user', user.id);
|
|
214
|
-
expect(found.name).toBe('Test User');
|
|
215
|
-
});
|
|
216
|
-
});
|
|
20
|
+
// Used internally by ObjectQL
|
|
217
21
|
```
|
|
218
|
-
|
|
219
|
-
## Relationship to Other Drivers
|
|
220
|
-
|
|
221
|
-
This driver serves as a reference implementation. For production use, consider:
|
|
222
|
-
|
|
223
|
-
- **@objectstack/driver-postgres** - PostgreSQL driver with full SQL capabilities
|
|
224
|
-
- **@objectstack/driver-mongodb** - MongoDB driver for document storage
|
|
225
|
-
- **@objectstack/driver-redis** - Redis driver for caching and key-value storage
|
|
226
|
-
|
|
227
|
-
## License
|
|
228
|
-
|
|
229
|
-
Apache-2.0
|
|
230
|
-
|
|
231
|
-
## Related Packages
|
|
232
|
-
|
|
233
|
-
- [@objectstack/runtime](../../runtime) - ObjectStack Runtime
|
|
234
|
-
- [@objectstack/spec](../../spec) - ObjectStack Specifications
|
|
235
|
-
- [@objectstack/core](../../core) - Core Interfaces and Types
|
package/dist/memory-driver.d.ts
CHANGED
|
@@ -54,6 +54,12 @@ export declare class InMemoryDriver implements DriverInterface {
|
|
|
54
54
|
updated_at: any;
|
|
55
55
|
id: any;
|
|
56
56
|
}[]>;
|
|
57
|
+
updateMany(object: string, query: QueryInput, data: Record<string, any>, options?: DriverOptions): Promise<{
|
|
58
|
+
count: number;
|
|
59
|
+
}>;
|
|
60
|
+
deleteMany(object: string, query: QueryInput, options?: DriverOptions): Promise<{
|
|
61
|
+
count: number;
|
|
62
|
+
}>;
|
|
57
63
|
bulkUpdate(object: string, updates: {
|
|
58
64
|
id: string | number;
|
|
59
65
|
data: Record<string, any>;
|
|
@@ -64,6 +70,9 @@ export declare class InMemoryDriver implements DriverInterface {
|
|
|
64
70
|
beginTransaction(): Promise<void>;
|
|
65
71
|
commit(): Promise<void>;
|
|
66
72
|
rollback(): Promise<void>;
|
|
73
|
+
private performAggregation;
|
|
74
|
+
private computeAggregate;
|
|
75
|
+
private setValueByPath;
|
|
67
76
|
private getTable;
|
|
68
77
|
private generateId;
|
|
69
78
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory-driver.d.ts","sourceRoot":"","sources":["../src/memory-driver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,eAAe,EAAwB,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"memory-driver.d.ts","sourceRoot":"","sources":["../src/memory-driver.ts"],"names":[],"mappings":"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"}
|
package/dist/memory-driver.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createLogger } from '@objectstack/core';
|
|
2
|
+
import { match, getValueByPath } from './memory-matcher.js';
|
|
2
3
|
/**
|
|
3
4
|
* Example: In-Memory Driver
|
|
4
5
|
*
|
|
@@ -13,10 +14,10 @@ export class InMemoryDriver {
|
|
|
13
14
|
// Transaction & Connection Management
|
|
14
15
|
transactions: false,
|
|
15
16
|
// Query Operations
|
|
16
|
-
queryFilters:
|
|
17
|
-
queryAggregations:
|
|
18
|
-
querySorting:
|
|
19
|
-
queryPagination: true, //
|
|
17
|
+
queryFilters: true, // Implemented via memory-matcher
|
|
18
|
+
queryAggregations: true, // Implemented
|
|
19
|
+
querySorting: true, // Implemented via JS sort
|
|
20
|
+
queryPagination: true, // Implemented
|
|
20
21
|
queryWindowFunctions: false, // TODO: Not implemented
|
|
21
22
|
querySubqueries: false, // TODO: Not implemented
|
|
22
23
|
joins: false, // TODO: Not implemented
|
|
@@ -81,9 +82,36 @@ export class InMemoryDriver {
|
|
|
81
82
|
async find(object, query, options) {
|
|
82
83
|
this.logger.debug('Find operation', { object, query });
|
|
83
84
|
const table = this.getTable(object);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
let results = table;
|
|
86
|
+
// 1. Filter
|
|
87
|
+
if (query.where) {
|
|
88
|
+
results = results.filter(record => match(record, query.where));
|
|
89
|
+
}
|
|
90
|
+
// 1.5 Aggregation & Grouping
|
|
91
|
+
if (query.groupBy || (query.aggregations && query.aggregations.length > 0)) {
|
|
92
|
+
results = this.performAggregation(results, query);
|
|
93
|
+
}
|
|
94
|
+
// 2. Sort
|
|
95
|
+
if (query.orderBy) {
|
|
96
|
+
// Normalize sort to array
|
|
97
|
+
const sortFields = Array.isArray(query.orderBy) ? query.orderBy : [query.orderBy];
|
|
98
|
+
results.sort((a, b) => {
|
|
99
|
+
for (const { field, order } of sortFields) {
|
|
100
|
+
const valA = getValueByPath(a, field);
|
|
101
|
+
const valB = getValueByPath(b, field);
|
|
102
|
+
if (valA === valB)
|
|
103
|
+
continue;
|
|
104
|
+
const comparison = valA > valB ? 1 : -1;
|
|
105
|
+
return order === 'desc' ? -comparison : comparison;
|
|
106
|
+
}
|
|
107
|
+
return 0;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
// 3. Pagination (Offset)
|
|
111
|
+
if (query.offset) {
|
|
112
|
+
results = results.slice(query.offset);
|
|
113
|
+
}
|
|
114
|
+
// 4. Pagination (Limit)
|
|
87
115
|
if (query.limit) {
|
|
88
116
|
results = results.slice(0, query.limit);
|
|
89
117
|
}
|
|
@@ -167,7 +195,11 @@ export class InMemoryDriver {
|
|
|
167
195
|
return true;
|
|
168
196
|
}
|
|
169
197
|
async count(object, query, options) {
|
|
170
|
-
|
|
198
|
+
let results = this.getTable(object);
|
|
199
|
+
if (query?.where) {
|
|
200
|
+
results = results.filter(record => match(record, query.where));
|
|
201
|
+
}
|
|
202
|
+
const count = results.length;
|
|
171
203
|
this.logger.debug('Count operation', { object, count });
|
|
172
204
|
return count;
|
|
173
205
|
}
|
|
@@ -180,6 +212,50 @@ export class InMemoryDriver {
|
|
|
180
212
|
this.logger.debug('BulkCreate completed', { object, count: results.length });
|
|
181
213
|
return results;
|
|
182
214
|
}
|
|
215
|
+
async updateMany(object, query, data, options) {
|
|
216
|
+
this.logger.debug('UpdateMany operation', { object, query });
|
|
217
|
+
const table = this.getTable(object);
|
|
218
|
+
let targetRecords = table;
|
|
219
|
+
if (query && query.where) {
|
|
220
|
+
targetRecords = targetRecords.filter(r => match(r, query.where));
|
|
221
|
+
}
|
|
222
|
+
const count = targetRecords.length;
|
|
223
|
+
// Update each record
|
|
224
|
+
for (const record of targetRecords) {
|
|
225
|
+
// Find index in original table
|
|
226
|
+
const index = table.findIndex(r => r.id === record.id);
|
|
227
|
+
if (index !== -1) {
|
|
228
|
+
const updated = {
|
|
229
|
+
...table[index],
|
|
230
|
+
...data,
|
|
231
|
+
updated_at: new Date()
|
|
232
|
+
};
|
|
233
|
+
table[index] = updated;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
this.logger.debug('UpdateMany completed', { object, count });
|
|
237
|
+
return { count };
|
|
238
|
+
}
|
|
239
|
+
async deleteMany(object, query, options) {
|
|
240
|
+
this.logger.debug('DeleteMany operation', { object, query });
|
|
241
|
+
const table = this.getTable(object);
|
|
242
|
+
const initialLength = table.length;
|
|
243
|
+
// Filter IN PLACE or create new array?
|
|
244
|
+
// Creating new array is safer for now.
|
|
245
|
+
const remaining = table.filter(r => {
|
|
246
|
+
if (!query || !query.where)
|
|
247
|
+
return false; // Delete all? No, standard safety implies explicit empty filter for delete all.
|
|
248
|
+
// Wait, normally deleteMany({}) deletes all.
|
|
249
|
+
// Let's assume if query passed, use it.
|
|
250
|
+
const matches = match(r, query.where);
|
|
251
|
+
return !matches; // Keep if it DOES NOT match
|
|
252
|
+
});
|
|
253
|
+
this.db[object] = remaining;
|
|
254
|
+
const count = initialLength - remaining.length;
|
|
255
|
+
this.logger.debug('DeleteMany completed', { object, count });
|
|
256
|
+
return { count };
|
|
257
|
+
}
|
|
258
|
+
// Compatibility aliases
|
|
183
259
|
async bulkUpdate(object, updates, options) {
|
|
184
260
|
this.logger.debug('BulkUpdate operation', { object, count: updates.length });
|
|
185
261
|
const results = await Promise.all(updates.map(u => this.update(object, u.id, u.data, options)));
|
|
@@ -213,6 +289,107 @@ export class InMemoryDriver {
|
|
|
213
289
|
async commit() { }
|
|
214
290
|
async rollback() { }
|
|
215
291
|
// ===================================
|
|
292
|
+
// Aggregation Logic
|
|
293
|
+
// ===================================
|
|
294
|
+
performAggregation(records, query) {
|
|
295
|
+
const { groupBy, aggregations } = query;
|
|
296
|
+
const groups = new Map();
|
|
297
|
+
// 1. Group records
|
|
298
|
+
if (groupBy && groupBy.length > 0) {
|
|
299
|
+
for (const record of records) {
|
|
300
|
+
// Create a composite key from group values
|
|
301
|
+
const keyParts = groupBy.map(field => {
|
|
302
|
+
const val = getValueByPath(record, field);
|
|
303
|
+
return val === undefined || val === null ? 'null' : String(val);
|
|
304
|
+
});
|
|
305
|
+
const key = JSON.stringify(keyParts);
|
|
306
|
+
if (!groups.has(key)) {
|
|
307
|
+
groups.set(key, []);
|
|
308
|
+
}
|
|
309
|
+
groups.get(key).push(record);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
// No grouping -> Single group containing all records
|
|
314
|
+
// If aggregation is requested without group by, it runs on whole set (even if empty)
|
|
315
|
+
if (aggregations && aggregations.length > 0) {
|
|
316
|
+
groups.set('all', records);
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
// Should not be here if performAggregation called correctly
|
|
320
|
+
groups.set('all', records);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// 2. Compute aggregates for each group
|
|
324
|
+
const resultRows = [];
|
|
325
|
+
for (const [key, groupRecords] of groups.entries()) {
|
|
326
|
+
const row = {};
|
|
327
|
+
// A. Add Group fields to row (if groupBy exists)
|
|
328
|
+
if (groupBy && groupBy.length > 0) {
|
|
329
|
+
if (groupRecords.length > 0) {
|
|
330
|
+
const firstRecord = groupRecords[0];
|
|
331
|
+
for (const field of groupBy) {
|
|
332
|
+
this.setValueByPath(row, field, getValueByPath(firstRecord, field));
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
// B. Compute Aggregations
|
|
337
|
+
if (aggregations) {
|
|
338
|
+
for (const agg of aggregations) {
|
|
339
|
+
const value = this.computeAggregate(groupRecords, agg);
|
|
340
|
+
row[agg.alias] = value;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
resultRows.push(row);
|
|
344
|
+
}
|
|
345
|
+
return resultRows;
|
|
346
|
+
}
|
|
347
|
+
computeAggregate(records, agg) {
|
|
348
|
+
const { function: func, field } = agg;
|
|
349
|
+
const values = field ? records.map(r => getValueByPath(r, field)) : [];
|
|
350
|
+
switch (func) {
|
|
351
|
+
case 'count':
|
|
352
|
+
if (!field || field === '*')
|
|
353
|
+
return records.length;
|
|
354
|
+
return values.filter(v => v !== null && v !== undefined).length;
|
|
355
|
+
case 'sum':
|
|
356
|
+
case 'avg': {
|
|
357
|
+
const nums = values.filter(v => typeof v === 'number');
|
|
358
|
+
const sum = nums.reduce((a, b) => a + b, 0);
|
|
359
|
+
if (func === 'sum')
|
|
360
|
+
return sum;
|
|
361
|
+
return nums.length > 0 ? sum / nums.length : null;
|
|
362
|
+
}
|
|
363
|
+
case 'min': {
|
|
364
|
+
// Handle comparable values
|
|
365
|
+
const valid = values.filter(v => v !== null && v !== undefined);
|
|
366
|
+
if (valid.length === 0)
|
|
367
|
+
return null;
|
|
368
|
+
// Works for numbers and strings
|
|
369
|
+
return valid.reduce((min, v) => (v < min ? v : min), valid[0]);
|
|
370
|
+
}
|
|
371
|
+
case 'max': {
|
|
372
|
+
const valid = values.filter(v => v !== null && v !== undefined);
|
|
373
|
+
if (valid.length === 0)
|
|
374
|
+
return null;
|
|
375
|
+
return valid.reduce((max, v) => (v > max ? v : max), valid[0]);
|
|
376
|
+
}
|
|
377
|
+
default:
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
setValueByPath(obj, path, value) {
|
|
382
|
+
const parts = path.split('.');
|
|
383
|
+
let current = obj;
|
|
384
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
385
|
+
const part = parts[i];
|
|
386
|
+
if (!current[part])
|
|
387
|
+
current[part] = {};
|
|
388
|
+
current = current[part];
|
|
389
|
+
}
|
|
390
|
+
current[parts[parts.length - 1]] = value;
|
|
391
|
+
}
|
|
392
|
+
// ===================================
|
|
216
393
|
// Helpers
|
|
217
394
|
// ===================================
|
|
218
395
|
getTable(name) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-driver.test.d.ts","sourceRoot":"","sources":["../src/memory-driver.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { InMemoryDriver } from './memory-driver.js';
|
|
3
|
+
describe('InMemoryDriver', () => {
|
|
4
|
+
let driver;
|
|
5
|
+
const testTable = 'test_table';
|
|
6
|
+
beforeEach(async () => {
|
|
7
|
+
driver = new InMemoryDriver();
|
|
8
|
+
await driver.connect();
|
|
9
|
+
// No explicit clear DB method exposed, but new instance is clean.
|
|
10
|
+
});
|
|
11
|
+
describe('Lifecycle', () => {
|
|
12
|
+
it('should connect successfully', async () => {
|
|
13
|
+
expect(driver.checkHealth()).resolves.toBe(true);
|
|
14
|
+
});
|
|
15
|
+
it('should clear data on disconnect', async () => {
|
|
16
|
+
await driver.create(testTable, { id: '1', name: 'test' });
|
|
17
|
+
await driver.disconnect();
|
|
18
|
+
const results = await driver.find(testTable, { fields: ['id'], object: testTable });
|
|
19
|
+
expect(results).toHaveLength(0);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
describe('Plugin Installation', () => {
|
|
23
|
+
it('should register driver with engine', () => {
|
|
24
|
+
const registerDriverFn = vi.fn();
|
|
25
|
+
const mockEngine = {
|
|
26
|
+
ql: {
|
|
27
|
+
registerDriver: registerDriverFn
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
driver.install({ engine: mockEngine });
|
|
31
|
+
expect(registerDriverFn).toHaveBeenCalledWith(driver);
|
|
32
|
+
});
|
|
33
|
+
it('should handle missing engine gracefully', () => {
|
|
34
|
+
const mockCtx = {}; // No engine
|
|
35
|
+
// Should not throw
|
|
36
|
+
driver.install(mockCtx);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
describe('CRUD Operations', () => {
|
|
40
|
+
it('should create and find records', async () => {
|
|
41
|
+
const data = { id: '1', name: 'Alice', age: 30 };
|
|
42
|
+
const created = await driver.create(testTable, data);
|
|
43
|
+
expect(created.id).toBe('1');
|
|
44
|
+
const results = await driver.find(testTable, {
|
|
45
|
+
fields: ['id', 'name', 'age'],
|
|
46
|
+
object: testTable
|
|
47
|
+
});
|
|
48
|
+
expect(results).toHaveLength(1);
|
|
49
|
+
expect(results[0].id).toBe('1');
|
|
50
|
+
expect(results[0].name).toBe('Alice');
|
|
51
|
+
});
|
|
52
|
+
it('should support updating records by ID', async () => {
|
|
53
|
+
await driver.create(testTable, { id: '1', name: 'Bob', active: true });
|
|
54
|
+
const updateResult = await driver.update(testTable, '1', { active: false });
|
|
55
|
+
expect(updateResult.active).toBe(false);
|
|
56
|
+
const results = await driver.find(testTable, { fields: ['active'], object: testTable });
|
|
57
|
+
expect(results[0].active).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
it('should support deleting records by ID', async () => {
|
|
60
|
+
await driver.create(testTable, { id: '1', name: 'Charlie' });
|
|
61
|
+
await driver.create(testTable, { id: '2', name: 'David' });
|
|
62
|
+
const deleteResult = await driver.delete(testTable, '1');
|
|
63
|
+
expect(deleteResult).toBe(true);
|
|
64
|
+
const results = await driver.find(testTable, { fields: ['name'], object: testTable });
|
|
65
|
+
expect(results).toHaveLength(1);
|
|
66
|
+
expect(results[0].name).toBe('David');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
describe('Query Capability', () => {
|
|
70
|
+
it('should filter results', async () => {
|
|
71
|
+
await driver.create(testTable, { id: '1', role: 'admin' });
|
|
72
|
+
await driver.create(testTable, { id: '2', role: 'user' });
|
|
73
|
+
await driver.create(testTable, { id: '3', role: 'user' });
|
|
74
|
+
const results = await driver.find(testTable, {
|
|
75
|
+
fields: ['id'],
|
|
76
|
+
object: testTable,
|
|
77
|
+
where: { role: 'user' }
|
|
78
|
+
});
|
|
79
|
+
expect(results).toHaveLength(2);
|
|
80
|
+
});
|
|
81
|
+
it('should limit results', async () => {
|
|
82
|
+
await driver.create(testTable, { id: '1' });
|
|
83
|
+
await driver.create(testTable, { id: '2' });
|
|
84
|
+
await driver.create(testTable, { id: '3' });
|
|
85
|
+
const results = await driver.find(testTable, {
|
|
86
|
+
fields: ['id'],
|
|
87
|
+
object: testTable,
|
|
88
|
+
limit: 2
|
|
89
|
+
});
|
|
90
|
+
expect(results).toHaveLength(2);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple In-Memory Query Matcher
|
|
3
|
+
*
|
|
4
|
+
* Implements a subset of the ObjectStack Filter Protocol (MongoDB-compatible)
|
|
5
|
+
* for evaluating conditions against in-memory JavaScript objects.
|
|
6
|
+
*/
|
|
7
|
+
type RecordType = Record<string, any>;
|
|
8
|
+
/**
|
|
9
|
+
* matches - Check if a record matches a filter criteria
|
|
10
|
+
* @param record The data record to check
|
|
11
|
+
* @param filter The filter condition (where clause)
|
|
12
|
+
*/
|
|
13
|
+
export declare function match(record: RecordType, filter: any): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Access nested properties via dot-notation (e.g. "user.name")
|
|
16
|
+
*/
|
|
17
|
+
export declare function getValueByPath(obj: any, path: string): any;
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=memory-matcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-matcher.d.ts","sourceRoot":"","sources":["../src/memory-matcher.ts"],"names":[],"mappings":"AACA;;;;;GAKG;AAEH,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAEtC;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,GAAG,OAAO,CAyC9D;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,GAAG,GAAG,CAG1D"}
|