Skip to content

Commit dafbcdf

Browse files
Copilothotlong
andcommitted
feat: add full-featured memory driver based on objectql reference
- Add MemoryConfigSchema, MemoryPersistenceConfigSchema, MemoryDriverSpec to packages/spec/src/data/driver/memory.zod.ts - Enhance InMemoryDriver with: initialData loading, strict mode, snapshot-based transactions, field projection, distinct values, clear/getSize utilities, counter-based ID generation, immutable record returns - Add comprehensive tests: 24 spec schema tests + 32 plugin implementation tests Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 4bb1e3b commit dafbcdf

5 files changed

Lines changed: 800 additions & 58 deletions

File tree

packages/plugins/driver-memory/src/index.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,7 @@
33
import { InMemoryDriver } from './memory-driver.js';
44

55
export { InMemoryDriver }; // Export class for direct usage
6-
7-
// Note: In a real environment, you would import these from @objectstack/spec
8-
// But distinct PluginDefinition interface might not be strictly defined in schema yet,
9-
// usually it mimics the Manifest structure + runtime hooks.
10-
11-
// For this example, we define a basic shape if not available, or use "any" to bypass strictness for the demo.
12-
// In plugin-bi it imported: import { PluginDefinition, PluginContextData } from '@objectstack/spec';
13-
14-
// Let's rely on the environment being set up like plugin-bi.
15-
// If types are missing, this is just an example file.
6+
export type { InMemoryDriverConfig } from './memory-driver.js';
167

178
export default {
189
id: 'com.objectstack.driver.memory',
@@ -22,10 +13,8 @@ export default {
2213
const { logger, config, drivers } = context;
2314
logger.info('[Memory Driver] Initializing...');
2415

25-
// Simulate driver registration
26-
// This assumes the runtime exposes a 'drivers' registry
2716
if (drivers) {
28-
const driver = new InMemoryDriver(config); // Pass config to driver
17+
const driver = new InMemoryDriver(config);
2918
drivers.register(driver);
3019
logger.info(`[Memory Driver] Registered driver: ${driver.name}`);
3120
} else {

packages/plugins/driver-memory/src/memory-driver.test.ts

Lines changed: 262 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ describe('InMemoryDriver', () => {
88
beforeEach(async () => {
99
driver = new InMemoryDriver();
1010
await driver.connect();
11-
// No explicit clear DB method exposed, but new instance is clean.
1211
});
1312

1413
describe('Lifecycle', () => {
@@ -86,6 +85,23 @@ describe('InMemoryDriver', () => {
8685
expect(results).toHaveLength(1);
8786
expect(results[0].name).toBe('David');
8887
});
88+
89+
it('should return a copy of created record (immutability)', async () => {
90+
const created = await driver.create(testTable, { id: '1', name: 'Alice' });
91+
created.name = 'Modified';
92+
93+
const found = await driver.find(testTable, { object: testTable });
94+
expect(found[0].name).toBe('Alice');
95+
});
96+
97+
it('should preserve created_at on update', async () => {
98+
const created = await driver.create(testTable, { id: '1', name: 'Alice' });
99+
const originalCreatedAt = created.created_at;
100+
101+
const updated = await driver.update(testTable, '1', { name: 'Alice Updated' });
102+
expect(updated.created_at).toBe(originalCreatedAt);
103+
expect(updated.name).toBe('Alice Updated');
104+
});
89105
});
90106

91107
describe('Query Capability', () => {
@@ -116,5 +132,250 @@ describe('InMemoryDriver', () => {
116132

117133
expect(results).toHaveLength(2);
118134
});
135+
136+
it('should project specific fields', async () => {
137+
await driver.create(testTable, { id: '1', name: 'Alice', age: 30, role: 'admin' });
138+
139+
const results = await driver.find(testTable, {
140+
fields: ['name', 'age'],
141+
object: testTable,
142+
});
143+
144+
expect(results).toHaveLength(1);
145+
expect(results[0].name).toBe('Alice');
146+
expect(results[0].age).toBe(30);
147+
expect(results[0].id).toBe('1'); // id always included
148+
expect(results[0].role).toBeUndefined();
149+
});
150+
});
151+
152+
describe('Initial Data', () => {
153+
it('should load initial data on connect', async () => {
154+
const driverWithData = new InMemoryDriver({
155+
initialData: {
156+
users: [
157+
{ id: '1', name: 'Alice' },
158+
{ id: '2', name: 'Bob' },
159+
],
160+
posts: [
161+
{ id: '1', title: 'Hello World' },
162+
],
163+
},
164+
});
165+
await driverWithData.connect();
166+
167+
const users = await driverWithData.find('users', { object: 'users' });
168+
expect(users).toHaveLength(2);
169+
expect(users[0].name).toBe('Alice');
170+
171+
const posts = await driverWithData.find('posts', { object: 'posts' });
172+
expect(posts).toHaveLength(1);
173+
});
174+
175+
it('should generate IDs for initial data without IDs', async () => {
176+
const driverWithData = new InMemoryDriver({
177+
initialData: {
178+
items: [{ name: 'Widget' }],
179+
},
180+
});
181+
await driverWithData.connect();
182+
183+
const items = await driverWithData.find('items', { object: 'items' });
184+
expect(items).toHaveLength(1);
185+
expect(items[0].id).toBeDefined();
186+
expect(typeof items[0].id).toBe('string');
187+
});
188+
});
189+
190+
describe('Strict Mode', () => {
191+
it('should throw on update of missing record in strict mode', async () => {
192+
const strictDriver = new InMemoryDriver({ strictMode: true });
193+
await strictDriver.connect();
194+
195+
await expect(
196+
strictDriver.update(testTable, 'non-existent', { name: 'Test' })
197+
).rejects.toThrow('Record with ID non-existent not found');
198+
});
199+
200+
it('should throw on delete of missing record in strict mode', async () => {
201+
const strictDriver = new InMemoryDriver({ strictMode: true });
202+
await strictDriver.connect();
203+
204+
await expect(
205+
strictDriver.delete(testTable, 'non-existent')
206+
).rejects.toThrow('Record with ID non-existent not found');
207+
});
208+
209+
it('should return null on update of missing record in default mode', async () => {
210+
const result = await driver.update(testTable, 'non-existent', { name: 'Test' });
211+
expect(result).toBeNull();
212+
});
213+
214+
it('should return false on delete of missing record in default mode', async () => {
215+
const result = await driver.delete(testTable, 'non-existent');
216+
expect(result).toBe(false);
217+
});
218+
});
219+
220+
describe('Transaction Support', () => {
221+
it('should begin and commit a transaction', async () => {
222+
await driver.create(testTable, { id: '1', name: 'Alice' });
223+
224+
const tx = await driver.beginTransaction();
225+
await driver.create(testTable, { id: '2', name: 'Bob' });
226+
await driver.commit(tx);
227+
228+
const results = await driver.find(testTable, { object: testTable });
229+
expect(results).toHaveLength(2);
230+
});
231+
232+
it('should rollback a transaction', async () => {
233+
await driver.create(testTable, { id: '1', name: 'Alice' });
234+
235+
const tx = await driver.beginTransaction();
236+
await driver.create(testTable, { id: '2', name: 'Bob' });
237+
238+
// Verify Bob exists before rollback
239+
let results = await driver.find(testTable, { object: testTable });
240+
expect(results).toHaveLength(2);
241+
242+
await driver.rollback(tx);
243+
244+
// After rollback, Bob should be gone
245+
results = await driver.find(testTable, { object: testTable });
246+
expect(results).toHaveLength(1);
247+
expect(results[0].name).toBe('Alice');
248+
});
249+
250+
it('should handle rollback of updates', async () => {
251+
await driver.create(testTable, { id: '1', name: 'Alice' });
252+
253+
const tx = await driver.beginTransaction();
254+
await driver.update(testTable, '1', { name: 'Alice Modified' });
255+
await driver.rollback(tx);
256+
257+
const results = await driver.find(testTable, { object: testTable });
258+
expect(results[0].name).toBe('Alice');
259+
});
260+
261+
it('should support capabilities.transactions = true', () => {
262+
expect(driver.supports.transactions).toBe(true);
263+
});
264+
});
265+
266+
describe('Utility Methods', () => {
267+
it('should clear all data', async () => {
268+
await driver.create(testTable, { id: '1', name: 'Alice' });
269+
await driver.create('other_table', { id: '1', name: 'Bob' });
270+
271+
expect(driver.getSize()).toBe(2);
272+
273+
await driver.clear();
274+
275+
expect(driver.getSize()).toBe(0);
276+
});
277+
278+
it('should return correct size', async () => {
279+
expect(driver.getSize()).toBe(0);
280+
281+
await driver.create(testTable, { id: '1' });
282+
expect(driver.getSize()).toBe(1);
283+
284+
await driver.create(testTable, { id: '2' });
285+
expect(driver.getSize()).toBe(2);
286+
287+
await driver.create('other', { id: '1' });
288+
expect(driver.getSize()).toBe(3);
289+
});
290+
291+
it('should return distinct values for a field', async () => {
292+
await driver.create(testTable, { id: '1', role: 'admin' });
293+
await driver.create(testTable, { id: '2', role: 'user' });
294+
await driver.create(testTable, { id: '3', role: 'user' });
295+
await driver.create(testTable, { id: '4', role: 'moderator' });
296+
297+
const roles = await driver.distinct(testTable, 'role');
298+
expect(roles).toHaveLength(3);
299+
expect(roles).toContain('admin');
300+
expect(roles).toContain('user');
301+
expect(roles).toContain('moderator');
302+
});
303+
304+
it('should return distinct values with filter', async () => {
305+
await driver.create(testTable, { id: '1', role: 'admin', active: true });
306+
await driver.create(testTable, { id: '2', role: 'user', active: false });
307+
await driver.create(testTable, { id: '3', role: 'user', active: true });
308+
309+
const roles = await driver.distinct(testTable, 'role', {
310+
object: testTable,
311+
where: { active: true },
312+
});
313+
expect(roles).toHaveLength(2);
314+
expect(roles).toContain('admin');
315+
expect(roles).toContain('user');
316+
});
317+
});
318+
319+
describe('Schema Management', () => {
320+
it('should create table on syncSchema', async () => {
321+
await driver.syncSchema('new_table', {});
322+
const results = await driver.find('new_table', { object: 'new_table' });
323+
expect(results).toHaveLength(0);
324+
});
325+
326+
it('should drop table', async () => {
327+
await driver.create(testTable, { id: '1', name: 'test' });
328+
await driver.dropTable(testTable);
329+
const results = await driver.find(testTable, { object: testTable });
330+
expect(results).toHaveLength(0);
331+
});
332+
});
333+
334+
describe('Bulk Operations', () => {
335+
it('should bulk create records', async () => {
336+
const records = [
337+
{ name: 'Alice' },
338+
{ name: 'Bob' },
339+
{ name: 'Charlie' },
340+
];
341+
const results = await driver.bulkCreate(testTable, records);
342+
expect(results).toHaveLength(3);
343+
expect(results[0].name).toBe('Alice');
344+
expect(results[1].name).toBe('Bob');
345+
});
346+
347+
it('should count records', async () => {
348+
await driver.create(testTable, { id: '1', role: 'admin' });
349+
await driver.create(testTable, { id: '2', role: 'user' });
350+
await driver.create(testTable, { id: '3', role: 'user' });
351+
352+
const total = await driver.count(testTable);
353+
expect(total).toBe(3);
354+
355+
const userCount = await driver.count(testTable, {
356+
object: testTable,
357+
where: { role: 'user' },
358+
});
359+
expect(userCount).toBe(2);
360+
});
361+
});
362+
363+
describe('ID Generation', () => {
364+
it('should generate unique counter-based IDs', async () => {
365+
const r1 = await driver.create(testTable, { name: 'A' });
366+
const r2 = await driver.create(testTable, { name: 'B' });
367+
368+
expect(r1.id).toBeDefined();
369+
expect(r2.id).toBeDefined();
370+
expect(r1.id).not.toBe(r2.id);
371+
// Counter-based IDs include the table name
372+
expect(r1.id).toContain(testTable);
373+
});
374+
});
375+
376+
describe('Version', () => {
377+
it('should report version 1.0.0', () => {
378+
expect(driver.version).toBe('1.0.0');
379+
});
119380
});
120381
});

0 commit comments

Comments
 (0)