Spaces:
Running
Running
Download source/teaql-ts/tests/checker.test.ts from teaql/browser-sqlite-playground: direct link, hf CLI and curl.
- Browser
- Download file 7.42 kB
-
https://huggingface.co/spaces/teaql/browser-sqlite-playground/resolve/main/source/teaql-ts/tests/checker.test.ts
- Command line
-
hf download hf://spaces/teaql/browser-sqlite-playground/source/teaql-ts/tests/checker.test.ts
-
curl -L -o checker.test.ts https://huggingface.co/spaces/teaql/browser-sqlite-playground/resolve/main/source/teaql-ts/tests/checker.test.ts
7.42 kB
| import { CheckException, CheckResult, EntityChecker, EntityRoot, MutationIntent, ObjectLocation, RuntimeModule, UserContext } from '../src'; | |
| import { AbstractSQLTeaQLClient, TeaQLSqlDriver } from '../src/sql/core'; | |
| class Driver implements TeaQLSqlDriver { | |
| readonly databaseKind = 'sqlite' as const; | |
| transactions = 0; | |
| statements: string[] = []; | |
| query = async (sql: string) => { this.statements.push(sql); return { rows: sql.startsWith('SELECT') ? [{ id: '1', version: 1, userEmail: 'a@b.test' }] : [], rowCount: 1 }; }; | |
| stream = async function* () {}; | |
| identifier = (value: string) => `"${value}"`; | |
| placeholder = () => '?'; | |
| encode = (value: any) => value; | |
| contains = () => ''; | |
| aggregateFunction = (name: string) => name; | |
| ensureSchema = async () => {}; | |
| transaction = async <T>(work: (session: any) => Promise<T>) => { this.transactions++; return work(this); }; | |
| nextId = async () => '1'; | |
| ensureIdFloor = async () => {}; | |
| close = async () => {}; | |
| } | |
| class Client extends AbstractSQLTeaQLClient { | |
| constructor(driver: Driver) { super(driver, {}); } | |
| } | |
| test('checker failure is structured, save scoped, and runs before SQL transaction', async () => { | |
| const driver = new Driver(); | |
| const context = new UserContext(); | |
| let calls = 0; | |
| const checker: EntityChecker = { | |
| checkAndFix(ctx, mutation, results: CheckResult[]) { | |
| calls++; | |
| expect(ctx.getResource('fixTime')).toBeInstanceOf(Date); | |
| if (!mutation.payload.name) results.push({ ruleId: 'required', location: ObjectLocation.property('name') }); | |
| }, | |
| }; | |
| const client = new Client(driver).setUserContext(context).install(new RuntimeModule({ | |
| Task: { table: 'task', columns: {} }, | |
| }, { Task: checker })); | |
| const mutation = { entity: 'Task', action: 'Create', payload: {}, comment: 'test' }; | |
| for (let attempt = 0; attempt < 2; attempt++) { | |
| await expect(client.executeMutation(mutation)).rejects.toBeInstanceOf(CheckException); | |
| } | |
| expect(calls).toBe(2); | |
| expect(driver.transactions).toBe(0); | |
| expect(context.getResource('fixTime')).toBeUndefined(); | |
| }); | |
| test('canonical multiword mutation key reaches checker and native SQL schema', async () => { | |
| const driver = new Driver(); | |
| const checker: EntityChecker = { | |
| checkAndFix(_context, mutation, results) { | |
| if (!mutation.payload.user_email) { | |
| results.push({ ruleId: 'required', location: ObjectLocation.property('user_email') }); | |
| } | |
| }, | |
| }; | |
| const client = new Client(driver).setUserContext(new UserContext()).install(new RuntimeModule({ | |
| User: { table: 'user_data', columns: { | |
| id: { columnName: 'id', logicalType: 'integer', decode: 'string' }, | |
| version: { columnName: 'version', logicalType: 'integer', decode: 'number' }, | |
| userEmail: { columnName: 'email_address', modelName: 'user_email', logicalType: 'text', decode: 'native' }, | |
| } }, | |
| }, { User: checker })); | |
| const result = await client.executeMutation({ | |
| entity: 'User', action: 'Create', payload: { user_email: 'a@b.test' }, comment: 'test canonical key', | |
| }); | |
| expect(result.persistedRecord?.userEmail).toBe('a@b.test'); | |
| expect(driver.statements.some(sql => sql.includes('"email_address"'))).toBe(true); | |
| }); | |
| test('update checker can add fixes to an immutable mutation ledger snapshot', async () => { | |
| const driver = new Driver(); | |
| const context = new UserContext(); | |
| const ledgerRoot = new EntityRoot(); | |
| const ledgerKey = { entity: 'Task', id: '1' } as const; | |
| const checker: EntityChecker = { | |
| checkAndFix(ctx, mutation) { | |
| expect(ctx.getResource('fixTime')).toBeInstanceOf(Date); | |
| mutation.payload.update_time = ctx.getResource('fixTime'); | |
| }, | |
| }; | |
| const client = new Client(driver).setUserContext(context).install(new RuntimeModule({ | |
| Task: { table: 'task_data', columns: { | |
| id: { columnName: 'id', logicalType: 'integer', decode: 'string' }, | |
| version: { columnName: 'version', logicalType: 'integer', decode: 'number' }, | |
| name: { columnName: 'name', logicalType: 'text', decode: 'native' }, | |
| updateTime: { columnName: 'update_time', modelName: 'update_time', logicalType: 'datetime', decode: 'date' }, | |
| } }, | |
| }, { Task: checker })); | |
| const immutableLedgerPayload = Object.freeze({ name: 'Updated task' }); | |
| await expect(client.executeMutation({ | |
| entity: 'Task', action: 'Update', payload: immutableLedgerPayload, | |
| id: '1', version: 1, comment: 'update task', ledgerKey, ledgerRoot, | |
| })).resolves.toBeDefined(); | |
| expect(Object.isFrozen(immutableLedgerPayload)).toBe(true); | |
| expect((immutableLedgerPayload as Record<string, unknown>).update_time).toBeUndefined(); | |
| expect(ledgerRoot.change(ledgerKey).update_time).toBeInstanceOf(Date); | |
| expect(driver.statements.some(sql => sql.includes('"update_time" = ?'))).toBe(true); | |
| }); | |
| test('one graph captures one fix clock and clears it after commit', async () => { | |
| const driver = new Driver(); | |
| const context = new UserContext(); | |
| const observed: Date[] = []; | |
| const checker: EntityChecker = { | |
| checkAndFix(ctx, mutation) { | |
| const now = ctx.requireResource<Date>('fixTime'); | |
| observed.push(now); | |
| mutation.payload.create_time = now; | |
| }, | |
| }; | |
| const client = new Client(driver).setUserContext(context).install(new RuntimeModule({ | |
| Task: { table: 'task_data', columns: { | |
| id: { columnName: 'id', logicalType: 'integer', decode: 'string' }, | |
| version: { columnName: 'version', logicalType: 'integer', decode: 'number' }, | |
| createTime: { columnName: 'create_time', modelName: 'create_time', logicalType: 'datetime', decode: 'date' }, | |
| } }, | |
| }, { Task: checker })); | |
| await client.executeGraphSave(new MutationIntent('fix graph'), async graph => { | |
| await client.executeMutation(graph.request({ entity: 'Task', action: 'Create', payload: {} })); | |
| await new Promise(resolve => setTimeout(resolve, 5)); | |
| await client.executeMutation(graph.request({ entity: 'Task', action: 'Create', payload: {} })); | |
| }); | |
| expect(observed).toHaveLength(2); | |
| expect(observed[0]).toBe(observed[1]); | |
| expect(driver.transactions).toBe(1); | |
| expect(context.getResource('fixTime')).toBeUndefined(); | |
| }); | |
| test('independent concurrent graph saves are serialized instead of joining', async () => { | |
| const driver = new Driver(); | |
| const client = new Client(driver).install(new RuntimeModule({ | |
| Task: { table: 'task_data', columns: { | |
| id: { columnName: 'id', logicalType: 'integer', decode: 'string' }, | |
| version: { columnName: 'version', logicalType: 'integer', decode: 'number' }, | |
| name: { columnName: 'name', logicalType: 'text', decode: 'string' }, | |
| } }, | |
| })); | |
| const events: string[] = []; | |
| const first = client.executeGraphSave(new MutationIntent('first'), async graph => { | |
| events.push('first:start'); | |
| await new Promise(resolve => setTimeout(resolve, 10)); | |
| await client.executeMutation(graph.request({ entity: 'Task', action: 'Create', payload: { name: 'first' } })); | |
| events.push('first:end'); | |
| }); | |
| const second = client.executeGraphSave(new MutationIntent('second'), async graph => { | |
| events.push('second:start'); | |
| await client.executeMutation(graph.request({ entity: 'Task', action: 'Create', payload: { name: 'second' } })); | |
| events.push('second:end'); | |
| }); | |
| await Promise.all([first, second]); | |
| expect(events).toEqual(['first:start', 'first:end', 'second:start', 'second:end']); | |
| expect(driver.transactions).toBe(2); | |
| }); | |