@objectstack/driver-memory 0.9.1 → 1.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/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # @objectstack/driver-memory
2
2
 
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - Major version release for ObjectStack Protocol v1.0.
8
+ - Stabilized Protocol Definitions
9
+ - Enhanced Runtime Plugin Support
10
+ - Fixed Type Compliance across Monorepo
11
+
12
+ ### Patch Changes
13
+
14
+ - Updated dependencies
15
+ - @objectstack/spec@1.0.0
16
+ - @objectstack/core@1.0.0
17
+
18
+ ## 0.9.2
19
+
20
+ ### Patch Changes
21
+
22
+ - Updated dependencies
23
+ - @objectstack/spec@0.9.2
24
+ - @objectstack/core@0.9.2
25
+
3
26
  ## 0.9.1
4
27
 
5
28
  ### Patch Changes
package/README.md CHANGED
@@ -1,227 +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
- ## Plugin Capabilities
6
-
7
- This driver implements the ObjectStack plugin capability protocol:
8
- - **Type**: `driver`
9
- - **Protocol**: `com.objectstack.protocol.storage.v1` (partial conformance)
10
- - **Provides**: `DriverInterface` for data storage operations
11
- - **Features**:
12
- - ✅ Basic CRUD operations
13
- - ✅ Pagination (limit/offset)
14
- - ❌ Advanced query filters
15
- - ❌ Aggregations
16
- - ❌ Sorting
17
- - ❌ Transactions
18
- - ❌ Joins
19
-
20
- 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.
21
4
 
22
5
  ## Features
23
6
 
24
- - 🚀 **Zero Dependencies**: Pure JavaScript implementation
25
- - 🧪 **Perfect for Testing**: Volatile storage ideal for unit tests
26
- - 📝 **TypeScript First**: Fully typed with TypeScript
27
- - 🔍 **Reference Implementation**: Clean example of DriverInterface
28
- - ⚡ **Fast**: In-memory operations are lightning fast
29
-
30
- ## Installation
31
-
32
- ```bash
33
- pnpm add @objectstack/driver-memory
34
- ```
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.
35
11
 
36
12
  ## Usage
37
13
 
38
- ### With ObjectStack Runtime
39
-
40
- ```typescript
41
- import { InMemoryDriver } from '@objectstack/driver-memory';
42
- import { DriverPlugin } from '@objectstack/runtime';
43
- import { ObjectKernel } from '@objectstack/runtime';
44
-
45
- const kernel = new ObjectKernel();
46
-
47
- // Create and register the driver
48
- const memoryDriver = new InMemoryDriver();
49
- kernel.use(new DriverPlugin(memoryDriver, 'memory'));
50
-
51
- await kernel.bootstrap();
52
- ```
53
-
54
- ### Standalone Usage
55
-
56
14
  ```typescript
57
15
  import { InMemoryDriver } from '@objectstack/driver-memory';
58
16
 
59
- const driver = new InMemoryDriver({
60
- seedData: true // Pre-populate with example data
61
- });
62
-
63
- // Initialize
17
+ const driver = new InMemoryDriver();
64
18
  await driver.connect();
65
19
 
66
- // Create a record
67
- const user = await driver.create('user', {
68
- name: 'John Doe',
69
- email: 'john@example.com'
70
- });
71
-
72
- // Find records
73
- const users = await driver.find('user', {
74
- limit: 10,
75
- offset: 0
76
- });
77
-
78
- // Get by ID
79
- const foundUser = await driver.findOne('user', user.id);
80
-
81
- // Update
82
- await driver.update('user', user.id, {
83
- name: 'Jane Doe'
84
- });
85
-
86
- // Delete
87
- await driver.delete('user', user.id);
88
-
89
- // Count
90
- const count = await driver.count('user');
91
-
92
- // Cleanup
93
- await driver.disconnect();
94
- ```
95
-
96
- ## API Reference
97
-
98
- ### InMemoryDriver
99
-
100
- The main driver class that implements `DriverInterface`.
101
-
102
- #### Constructor Options
103
-
104
- ```typescript
105
- interface DriverOptions {
106
- /**
107
- * Pre-populate the database with example data on startup
108
- * @default false
109
- */
110
- seedData?: boolean;
111
-
112
- /**
113
- * Logger instance
114
- */
115
- logger?: Logger;
116
- }
117
- ```
118
-
119
- #### Methods
120
-
121
- - `connect()` - Initialize the driver (no-op for in-memory)
122
- - `disconnect()` - Cleanup resources (clears all data)
123
- - `create(object, data)` - Create a new record
124
- - `find(object, query?)` - Query records with optional pagination
125
- - `findOne(object, id)` - Get a single record by ID
126
- - `update(object, id, data)` - Update a record
127
- - `delete(object, id)` - Delete a record
128
- - `count(object, query?)` - Count total records
129
- - `getSchema(object)` - Get object schema definition
130
- - `query(query)` - Execute a raw query (limited support)
131
-
132
- #### Capabilities
133
-
134
- The driver declares its capabilities via the `supports` property:
135
-
136
- ```typescript
137
- {
138
- transactions: false,
139
- queryFilters: false,
140
- queryAggregations: false,
141
- querySorting: false,
142
- queryPagination: true, // ✅ Supported via limit/offset
143
- queryWindowFunctions: false,
144
- querySubqueries: false,
145
- joins: false,
146
- fullTextSearch: false,
147
- vectorSearch: false,
148
- geoSpatial: false
149
- }
150
- ```
151
-
152
- ## Data Storage
153
-
154
- The in-memory driver stores data in a simple Map structure:
155
-
156
- ```typescript
157
- private tables: Map<string, Array<Record<string, any>>> = new Map();
20
+ // Used internally by ObjectQL
158
21
  ```
159
-
160
- **Important**: All data is lost when the process exits or `disconnect()` is called. This driver is **not suitable for production use**.
161
-
162
- ## Use Cases
163
-
164
- ✅ **Good for:**
165
- - Unit testing
166
- - Integration testing
167
- - Development/prototyping
168
- - CI/CD pipelines
169
- - Examples and tutorials
170
- - Learning ObjectStack
171
-
172
- ❌ **Not suitable for:**
173
- - Production environments
174
- - Data persistence requirements
175
- - Large datasets (memory constraints)
176
- - Multi-process scenarios
177
- - Concurrent write operations
178
-
179
- ## Testing Example
180
-
181
- ```typescript
182
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
183
- import { InMemoryDriver } from '@objectstack/driver-memory';
184
-
185
- describe('User CRUD', () => {
186
- let driver: InMemoryDriver;
187
-
188
- beforeEach(async () => {
189
- driver = new InMemoryDriver();
190
- await driver.connect();
191
- });
192
-
193
- afterEach(async () => {
194
- await driver.disconnect();
195
- });
196
-
197
- it('should create and retrieve a user', async () => {
198
- const user = await driver.create('user', {
199
- name: 'Test User',
200
- email: 'test@example.com'
201
- });
202
-
203
- expect(user.id).toBeDefined();
204
-
205
- const found = await driver.findOne('user', user.id);
206
- expect(found.name).toBe('Test User');
207
- });
208
- });
209
- ```
210
-
211
- ## Relationship to Other Drivers
212
-
213
- This driver serves as a reference implementation. For production use, consider:
214
-
215
- - **@objectstack/driver-postgres** - PostgreSQL driver with full SQL capabilities
216
- - **@objectstack/driver-mongodb** - MongoDB driver for document storage
217
- - **@objectstack/driver-redis** - Redis driver for caching and key-value storage
218
-
219
- ## License
220
-
221
- Apache-2.0
222
-
223
- ## Related Packages
224
-
225
- - [@objectstack/runtime](../../runtime) - ObjectStack Runtime
226
- - [@objectstack/spec](../../spec) - ObjectStack Specifications
227
- - [@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"}