feat(storage): implement SQLite adapter with better-sqlite3
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
201
packages/storage/src/adapters/sqlite.test.ts
Normal file
201
packages/storage/src/adapters/sqlite.test.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { SqliteAdapter } from './sqlite.js';
|
||||
|
||||
describe('SqliteAdapter', () => {
|
||||
let adapter: SqliteAdapter;
|
||||
|
||||
beforeEach(async () => {
|
||||
adapter = new SqliteAdapter({ type: 'sqlite', path: ':memory:' });
|
||||
await adapter.migrate();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await adapter.close();
|
||||
});
|
||||
|
||||
describe('CRUD', () => {
|
||||
it('creates and reads a record', async () => {
|
||||
const created = await adapter.create('users', { name: 'Alice', email: 'alice@test.com' });
|
||||
expect(created.id).toBeDefined();
|
||||
expect(created.name).toBe('Alice');
|
||||
|
||||
const read = await adapter.read('users', created.id);
|
||||
expect(read).not.toBeNull();
|
||||
expect(read!.name).toBe('Alice');
|
||||
expect(read!.email).toBe('alice@test.com');
|
||||
});
|
||||
|
||||
it('returns null for non-existent record', async () => {
|
||||
const result = await adapter.read('users', 'does-not-exist');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('updates a record', async () => {
|
||||
const created = await adapter.create('users', { name: 'Alice' });
|
||||
const updated = await adapter.update('users', created.id, { name: 'Bob' });
|
||||
expect(updated).toBe(true);
|
||||
|
||||
const read = await adapter.read('users', created.id);
|
||||
expect(read!.name).toBe('Bob');
|
||||
});
|
||||
|
||||
it('update returns false for non-existent record', async () => {
|
||||
const result = await adapter.update('users', 'does-not-exist', { name: 'X' });
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('deletes a record', async () => {
|
||||
const created = await adapter.create('users', { name: 'Alice' });
|
||||
const deleted = await adapter.delete('users', created.id);
|
||||
expect(deleted).toBe(true);
|
||||
|
||||
const read = await adapter.read('users', created.id);
|
||||
expect(read).toBeNull();
|
||||
});
|
||||
|
||||
it('delete returns false for non-existent record', async () => {
|
||||
const result = await adapter.delete('users', 'does-not-exist');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('find', () => {
|
||||
it('finds records with filter', async () => {
|
||||
await adapter.create('users', { name: 'Alice', role: 'admin' });
|
||||
await adapter.create('users', { name: 'Bob', role: 'user' });
|
||||
await adapter.create('users', { name: 'Charlie', role: 'admin' });
|
||||
|
||||
const admins = await adapter.find('users', { role: 'admin' });
|
||||
expect(admins).toHaveLength(2);
|
||||
expect(admins.map((u) => u.name).sort()).toEqual(['Alice', 'Charlie']);
|
||||
});
|
||||
|
||||
it('finds all records without filter', async () => {
|
||||
await adapter.create('users', { name: 'Alice' });
|
||||
await adapter.create('users', { name: 'Bob' });
|
||||
|
||||
const all = await adapter.find('users');
|
||||
expect(all).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('supports limit and offset', async () => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await adapter.create('users', { name: `User${i}`, idx: i });
|
||||
}
|
||||
|
||||
const page = await adapter.find('users', undefined, {
|
||||
limit: 2,
|
||||
offset: 1,
|
||||
orderBy: 'created_at',
|
||||
});
|
||||
expect(page).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('findOne returns first match', async () => {
|
||||
await adapter.create('users', { name: 'Alice', role: 'admin' });
|
||||
await adapter.create('users', { name: 'Bob', role: 'user' });
|
||||
|
||||
const found = await adapter.findOne('users', { role: 'user' });
|
||||
expect(found).not.toBeNull();
|
||||
expect(found!.name).toBe('Bob');
|
||||
});
|
||||
|
||||
it('findOne returns null when no match', async () => {
|
||||
const result = await adapter.findOne('users', { role: 'nonexistent' });
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('count', () => {
|
||||
it('counts all records', async () => {
|
||||
await adapter.create('users', { name: 'Alice' });
|
||||
await adapter.create('users', { name: 'Bob' });
|
||||
|
||||
const total = await adapter.count('users');
|
||||
expect(total).toBe(2);
|
||||
});
|
||||
|
||||
it('counts with filter', async () => {
|
||||
await adapter.create('users', { name: 'Alice', role: 'admin' });
|
||||
await adapter.create('users', { name: 'Bob', role: 'user' });
|
||||
await adapter.create('users', { name: 'Charlie', role: 'admin' });
|
||||
|
||||
const adminCount = await adapter.count('users', { role: 'admin' });
|
||||
expect(adminCount).toBe(2);
|
||||
});
|
||||
|
||||
it('returns 0 for empty collection', async () => {
|
||||
const count = await adapter.count('users');
|
||||
expect(count).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transaction', () => {
|
||||
it('commits on success', async () => {
|
||||
await adapter.transaction(async (tx) => {
|
||||
await tx.create('users', { name: 'Alice' });
|
||||
await tx.create('users', { name: 'Bob' });
|
||||
});
|
||||
|
||||
const count = await adapter.count('users');
|
||||
expect(count).toBe(2);
|
||||
});
|
||||
|
||||
it('rolls back on error', async () => {
|
||||
await expect(
|
||||
adapter.transaction(async (tx) => {
|
||||
await tx.create('users', { name: 'Alice' });
|
||||
throw new Error('rollback test');
|
||||
}),
|
||||
).rejects.toThrow('rollback test');
|
||||
|
||||
const count = await adapter.count('users');
|
||||
expect(count).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('migrate', () => {
|
||||
it('creates all tables', async () => {
|
||||
// migrate() was already called in beforeEach — verify tables exist
|
||||
const collections = [
|
||||
'users',
|
||||
'sessions',
|
||||
'accounts',
|
||||
'projects',
|
||||
'missions',
|
||||
'tasks',
|
||||
'agents',
|
||||
'conversations',
|
||||
'messages',
|
||||
'preferences',
|
||||
'insights',
|
||||
'skills',
|
||||
'events',
|
||||
'routing_rules',
|
||||
'provider_credentials',
|
||||
'agent_logs',
|
||||
'teams',
|
||||
'team_members',
|
||||
'mission_tasks',
|
||||
'tickets',
|
||||
'summarization_jobs',
|
||||
'appreciations',
|
||||
'verifications',
|
||||
];
|
||||
|
||||
for (const collection of collections) {
|
||||
// Should not throw
|
||||
const count = await adapter.count(collection);
|
||||
expect(count).toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('is idempotent', async () => {
|
||||
await adapter.migrate();
|
||||
await adapter.migrate();
|
||||
// Should not throw
|
||||
const count = await adapter.count('users');
|
||||
expect(count).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user