browser-sqlite-playground / source /teaql-ts /tests /browser-sqlite.test.ts
philipgreat's picture
Deploy traceable order management playground
aa89cb5 verified
Raw History Blame Contribute Delete
5.39 kB
import { UserContext } from '../src/core/context';
import {
DelegatingMutationPolicyRegistry,
MutationPolicyError,
} from '../src/core/mutation-policy';
import {
BrowserSQLiteDriver,
BrowserSQLiteTeaQLClient,
BrowserSQLiteWorkerLike,
} from '../src/sql/browser-sqlite';
import {
BrowserSQLiteRequest,
BrowserSQLiteResponse,
} from '../src/sql/browser-sqlite-protocol';
class FakeWorker implements BrowserSQLiteWorkerLike {
readonly requests: BrowserSQLiteRequest[] = [];
terminated = false;
private readonly messageListeners = new Set<(event: MessageEvent<BrowserSQLiteResponse>) => void>();
private readonly errorListeners = new Set<(event: ErrorEvent) => void>();
postMessage(message: BrowserSQLiteRequest): void {
this.requests.push(message);
queueMicrotask(() => {
let result: unknown;
if (message.operation === 'query') {
const sql = String(message.payload?.sql ?? '');
result = sql.startsWith('SELECT')
? { rows: [{ id: 1, name: 'Local' }], rowCount: 1 }
: { rows: [], rowCount: 0 };
}
const event = { data: {
type: 'teaql-browser-sqlite-response', requestId: message.requestId, result,
} } as MessageEvent<BrowserSQLiteResponse>;
for (const listener of this.messageListeners) listener(event);
});
}
addEventListener(type: 'message' | 'error', listener: any): void {
if (type === 'message') this.messageListeners.add(listener);
else this.errorListeners.add(listener);
}
removeEventListener(type: 'message' | 'error', listener: any): void {
if (type === 'message') this.messageListeners.delete(listener);
else this.errorListeners.delete(listener);
}
terminate(): void { this.terminated = true; }
}
describe('BrowserSQLiteDriver', () => {
test('defaults to a deterministic in-memory database', async () => {
const worker = new FakeWorker();
const driver = await BrowserSQLiteDriver.open(worker);
expect(worker.requests[0]).toMatchObject({
operation: 'open',
payload: { storage: 'memory', databaseName: 'teaql-browser.sqlite3' },
});
await driver.close();
expect(worker.terminated).toBe(true);
});
test('supports explicit OPFS and buffered stream compatibility', async () => {
const worker = new FakeWorker();
const driver = await BrowserSQLiteDriver.open(worker, {
storage: 'opfs', databaseName: 'demo.sqlite3', terminateWorkerOnClose: false,
});
const rows: unknown[] = [];
for await (const row of driver.stream('SELECT id, name FROM demo')) rows.push(row);
expect(rows).toEqual([{ id: 1, name: 'Local' }]);
expect(worker.requests[0].payload).toMatchObject({ storage: 'opfs', databaseName: 'demo.sqlite3' });
await driver.close();
expect(worker.terminated).toBe(false);
});
test('serializes a transaction and rolls it back on failure', async () => {
const worker = new FakeWorker();
const driver = await BrowserSQLiteDriver.open(worker);
await expect(driver.transaction(async session => {
await session.query('UPDATE demo SET name = ?', ['Changed']);
throw new Error('reject mutation');
})).rejects.toThrow('reject mutation');
expect(worker.requests.filter(request => request.operation === 'query')
.map(request => request.payload?.sql)).toEqual([
'BEGIN IMMEDIATE', 'UPDATE demo SET name = ?', 'ROLLBACK',
]);
await driver.close();
});
test('reset invalidates schema state and explicitly reconciles it again', async () => {
const worker = new FakeWorker();
const client = await BrowserSQLiteTeaQLClient.open(worker, {});
const context = new UserContext().insertResource('dataService', client);
client.setUserContext(context);
await context.ensureSchema();
await client.reset(context);
expect(worker.requests.filter(request => request.operation === 'reset')).toHaveLength(1);
expect(worker.requests.filter(request =>
request.operation === 'query' && String(request.payload?.sql).includes('teaql_id_space'),
)).toHaveLength(2);
await client.close();
});
test('rejects path-like database names', async () => {
await expect(BrowserSQLiteDriver.open(new FakeWorker(), {
databaseName: '../outside.sqlite3',
})).rejects.toThrow('simple file name');
});
test.each(['memory', 'opfs'] as const)(
'%s profile applies Mutation Policy before sending SQL to the worker',
async storage => {
const worker = new FakeWorker();
const client = await BrowserSQLiteTeaQLClient.open(worker, {}, { storage });
const context = new UserContext().withMutationPolicyRegistry(
new DelegatingMutationPolicyRegistry(() => ({
identity: { policyId: 'browser-demo', version: '1', fingerprint: 'sha256:demo' },
review: () => ({ verdict: 'deny', code: 'DEMO_DENIED', message: 'disabled' }),
})),
);
client.setUserContext(context);
const queriesBefore = worker.requests.filter(request => request.operation === 'query').length;
await expect(client.executeMutation({
entity: 'Demo', action: 'Create', payload: { name: 'blocked' }, comment: 'demo',
})).rejects.toThrow(MutationPolicyError);
expect(worker.requests.filter(request => request.operation === 'query')).toHaveLength(queriesBefore);
await client.close();
},
);
});