@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 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 Driver for ObjectStack. A reference implementation of the DriverInterface that stores data in memory using JavaScript arrays.
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
- - ๐Ÿš€ **Zero Dependencies**: Pure JavaScript implementation
33
- - ๐Ÿงช **Perfect for Testing**: Volatile storage ideal for unit tests
34
- - ๐Ÿ“ **TypeScript First**: Fully typed with TypeScript
35
- - ๐Ÿ” **Reference Implementation**: Clean example of DriverInterface
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
- // Create a record
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
@@ -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;AAE1E;;;;;GAKG;AACH,qBAAa,cAAe,YAAW,eAAe;IACpD,IAAI,SAAsB;IAC1B,OAAO,SAAW;IAClB,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,MAAM,CAAS;gBAEX,MAAM,CAAC,EAAE,GAAG;IAOxB,OAAO,CAAC,GAAG,EAAE,GAAG;IAUhB,QAAQ;;;;;;;;;;;;;;MAmBN;IAEF;;OAEG;IACH,OAAO,CAAC,EAAE,CAA6B;IAMjC,OAAO;IAIP,UAAU;IAWV,WAAW;IAYX,OAAO,CAAC,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE;IASpC,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE,aAAa;IAiB9D,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE,aAAa;IAStE,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE,aAAa;IAUlE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO,CAAC,EAAE,aAAa;;;;;IAkBzE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO,CAAC,EAAE,aAAa;IAsB9F,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,YAAY,CAAC,EAAE,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE,aAAa;IAqBlG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa;IAgBnE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE,aAAa;IAUjE,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,EAAE,OAAO,CAAC,EAAE,aAAa;;;;;IAOpF,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;KAAE,EAAE,EAAE,OAAO,CAAC,EAAE,aAAa;IAOjH,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,EAAE,OAAO,CAAC,EAAE,aAAa;IAU5E,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,aAAa;IAO/D,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa;IAQjD,gBAAgB;IAIhB,MAAM;IACN,QAAQ;IAMd,OAAO,CAAC,QAAQ;IAOhB,OAAO,CAAC,UAAU;CAGnB"}
1
+ {"version":3,"file":"memory-driver.d.ts","sourceRoot":"","sources":["../src/memory-driver.ts"],"names":[],"mappings":"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"}
@@ -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: false, // TODO: Not implemented - basic find() doesn't handle filters
17
- queryAggregations: false, // TODO: Not implemented - count() only returns total
18
- querySorting: false, // TODO: Not implemented - find() doesn't handle sorting
19
- queryPagination: true, // Basic pagination via 'limit' is implemented
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
- // ๐Ÿ’ก Naive Implementation
85
- let results = [...table];
86
- // Simple limiting for demonstration
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
- const count = this.getTable(object).length;
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=memory-driver.test.d.ts.map
@@ -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"}