UNPKG

@instantdb/core

Version:
451 lines (375 loc) • 11.3 kB
import { i } from '../../src/schema'; import { validateTransactions } from '../../src/transactionValidation.ts'; import { lookup, tx as originalTx, TxChunk } from '../../src/instatx.ts'; import id from '../../src/utils/id.ts'; import { expect, test } from 'vitest'; import { InstantSchemaDef } from '../../src'; const testSchema = i.schema({ entities: { users: i.entity({ name: i.string(), email: i.string().indexed().unique(), bio: i.string().optional(), stuff: i.json<{ custom: string }>().optional(), junk: i.any().optional(), }), posts: i.entity({ title: i.string(), body: i.string(), }), comments: i.entity({ body: i.string(), }), unlinkedWithAnything: i.entity({ animal: i.string(), count: i.string(), }), }, links: { usersPosts: { forward: { on: 'users', has: 'many', label: 'posts', }, reverse: { on: 'posts', has: 'one', label: 'author', }, }, postsComments: { forward: { on: 'posts', has: 'many', label: 'comments', }, reverse: { on: 'comments', has: 'one', label: 'post', }, }, friendships: { forward: { on: 'users', has: 'many', label: 'friends', }, reverse: { on: 'users', has: 'many', label: '_friends', }, }, }, }); const beValid = ( chunk: any, schema: InstantSchemaDef<any, any, any> | null = testSchema, ) => { expect(() => validateTransactions(chunk, schema ?? undefined)).not.toThrow(); if (schema) { expect(() => validateTransactions(chunk, undefined)).not.toThrow(); } }; const beWrong = ( chunk: any, schema: InstantSchemaDef<any, any, any> | null = testSchema, ) => { expect(() => validateTransactions(chunk, schema ?? undefined)).toThrow(); }; const tx = originalTx as unknown as TxChunk<typeof testSchema>; test('validates basic transaction chunk', () => { const userId = id(); const validChunk = tx.users[userId].create({ name: 'John', email: 'john@example.com', }); beValid(validChunk); }); test('validates transaction chunk arrays', () => { const userId = id(); const postId = id(); const chunks = [ tx.users[userId].create({ name: 'John', email: 'john@example.com' }), tx.posts[postId].create({ title: 'Hello', body: 'World' }), ]; beValid(chunks); }); test('validates create operations', () => { const userId = id(); // Valid create beValid(tx.users[userId].create({ name: 'John', email: 'john@example.com' })); // Valid create with optional field beValid( tx.users[userId].create({ name: 'John', email: 'john@example.com', bio: 'Developer', }), ); // Valid create with any type beValid( tx.users[userId].create({ name: 'John', email: 'john@example.com', junk: { anything: 'goes' }, }), ); // Invalid create - wrong type // @ts-expect-error beWrong(tx.users[userId].create({ name: 123, email: 'john@example.com' })); // Valid create - creates unknown attributes beValid( tx.users[userId].create({ name: 'John', email: 'john@example.com', // @ts-expect-error unknownField: 'value', }), ); // Invalid create - non-object args beWrong({ __ops: [['create', 'users', userId, 'not an object']], __etype: 'users', }); }); test('validates update operations', () => { const userId = id(); // Valid update beValid(tx.users[userId].update({ name: 'Jane' })); // Valid update with multiple fields beValid(tx.users[userId].update({ name: 'Jane', bio: 'Updated bio' })); // Invalid update - wrong type // @ts-expect-error beWrong(tx.users[userId].update({ name: 123 })); // Invalid update - unknown attribute // @ts-expect-error beValid(tx.users[userId].update({ unknownField: 'value' })); }); test('validates merge operations', () => { const userId = id(); // Valid merge beValid(tx.users[userId].merge({ stuff: { custom: 'value' } })); // Invalid merge - wrong type beWrong(tx.users[userId].merge({ name: 123 })); }); test('validates delete operations', () => { const userId = id(); // Valid delete beValid(tx.users[userId].delete()); }); test('validates link operations', () => { const userId = id(); const postId = id(); // Valid link beValid(tx.users[userId].link({ posts: postId })); // Valid link with array beValid(tx.users[userId].link({ posts: [postId, id()] })); // Invalid link - unknown link // @ts-expect-error beWrong(tx.users[userId].link({ unknownLink: postId })); // Invalid link - non-object args beWrong({ __ops: [['link', 'users', userId, 'not an object']], __etype: 'users', }); }); test('validates unlink operations', () => { const userId = id(); const postId = id(); // Valid unlink beValid(tx.users[userId].unlink({ posts: postId })); // Valid unlink with array beValid(tx.users[userId].unlink({ posts: [postId, id()] })); // Invalid unlink - unknown link // @ts-expect-error beWrong(tx.users[userId].unlink({ unknownLink: postId })); }); test('validates entity existence', () => { const unknownId = id(); // Invalid entity beWrong({ __ops: [['create', 'unknownNamespace', unknownId, { field: 'value' }]], __etype: 'unknownNamespace', }); // Valid without schema beValid( { __ops: [['create', 'unknownNamespace', unknownId, { field: 'value' }]], __etype: 'unknownNamespace', }, null, ); }); test('validates attribute types', () => { const userId = id(); // Valid string beValid(tx.users[userId].create({ name: 'John', email: 'john@example.com' })); // Invalid string - number // @ts-expect-error beWrong(tx.users[userId].create({ name: 123, email: 'john@example.com' })); // Invalid string - boolean // @ts-expect-error beWrong(tx.users[userId].create({ name: true, email: 'john@example.com' })); // Valid any type beValid( tx.users[userId].create({ name: 'John', email: 'john@example.com', junk: 'this is the junk type', }), ); beValid( tx.users[userId].create({ name: 'John', email: 'john@example.com', junk: 123, }), ); beValid( tx.users[userId].create({ name: 'John', email: 'john@example.com', junk: { complex: 'object' }, }), ); }); test('validates transaction chunk structure', () => { // Invalid chunk - not an object beWrong('not an object'); beWrong(123); beWrong(null); // Invalid chunk - missing __ops beWrong({ __etype: 'users' }); // Invalid chunk - __ops not an array beWrong({ __ops: 'not an array', __etype: 'users' }); // Invalid operation - not an array beWrong({ __ops: ['not an array'], __etype: 'users' }); }); test('validates operation structure', () => { const userId = id(); // Invalid entity name - not a string beWrong({ __ops: [['create', 123, userId, {}]], __etype: 'users' }); }); test('validates chained operations', () => { const userId = id(); const postId = id(); // Valid chained operations beValid( tx.users[userId] .create({ name: 'John', email: 'john@example.com' }) .link({ posts: postId }), ); // Valid complex chain beValid( tx.users[userId] .create({ name: 'John', email: 'john@example.com' }) .link({ posts: postId }) .update({ bio: 'Updated' }), ); }); test('validates multiple entity types', () => { const userId = id(); const postId = id(); const commentId = id(); const chunks = [ tx.users[userId].create({ name: 'John', email: 'john@example.com' }), tx.posts[postId].create({ title: 'Hello', body: 'World' }), tx.comments[commentId].create({ body: 'Nice post!' }), tx.posts[postId].link({ comments: commentId }), tx.users[userId].link({ posts: postId }), ]; beValid(chunks); }); test('validates link relationships', () => { const userId = id(); const postId = id(); const commentId = id(); // Valid link between users and posts beValid(tx.users[userId].link({ posts: postId })); // Valid link between posts and comments beValid(tx.posts[postId].link({ comments: commentId })); // Valid self-referential link (friendships) beValid(tx.users[userId].link({ friends: id() })); // Invalid link - no relationship exists // @ts-expect-error beWrong(tx.users[userId].link({ unlinkedWithAnything: id() })); }); test('validates without schema', () => { const userId = id(); // Should not throw without schema beValid( originalTx.randomEntity[userId].create({ anyField: 'anyValue' }), null, ); beValid(originalTx.randomEntity[userId].update({ anyField: 123 }), null); beValid(originalTx.randomEntity[userId].link({ anyLink: id() }), null); }); test('validates UUID format for entity IDs', () => { // Valid UUID should pass const validUuid = id(); beValid( tx.users[validUuid].create({ name: 'John', email: 'john@example.com' }), ); beWrong( tx.users['not a valid uuid'].create({ name: 'John', email: 'john@example.com', }), ); // Test for links beValid(tx.users[validUuid].link({ posts: id() })); beWrong(tx.users[validUuid].link({ posts: 'not-a-uuid' })); }); test('allows lookup values in square bracket', () => { beValid( tx.users[lookup('email', 'john@example.net')].update({ name: 'John' }), ); beValid(tx.users[lookup('email', 'john@example.net')].link({ posts: id() })); }); test('allows lookup values in link', () => { beValid(tx.users[id()].link({ posts: lookup('title', 'Hello') })); beWrong(tx.users[id()].link({ posts: 'non lookup or uuid' })); }); test('lookup proxy', () => { const dbTx = tx.users.lookup('email', 'dsharris').update({ email: 'dsharris@example.com', name: 'David Harris', }); const oldVersion = tx.users[lookup('email', 'dsharris')].update({ email: 'dsharris@example.com', name: 'David Harris', }); expect(oldVersion).toEqual(dbTx); const animalSchema = i.schema({ entities: { otter: i.entity({ name: i.string(), uniqueName: i.string().unique(), uniqueDate: i.date().unique(), }), elephant: i.entity({ name: i.string(), uniqueIdNumber: i.string().unique(), favoriteColor: i.string(), }), }, }); const animalTx = originalTx as unknown as TxChunk<typeof animalSchema>; const otterNameUnique = animalTx.otter.lookup('uniqueName', '8932'); // Note: doesn't allow date type, would have to do a lot of threading to implement, easier to change useDates default const otterDateUnique = animalTx.otter.lookup('uniqueDate', '8932'); expect(otterNameUnique).toEqual(animalTx.otter[lookup('uniqueName', '8932')]); expect(otterDateUnique).toEqual(animalTx.otter[lookup('uniqueDate', '8932')]); const elephantIdNumberUnique = animalTx.elephant.lookup( 'uniqueIdNumber', '1234567890', ); // @ts-expect-error const _invalidLookup = animalTx.elephant.lookup('favoriteColor', 'blue'); expect(elephantIdNumberUnique).toEqual( animalTx.elephant[lookup('uniqueIdNumber', '1234567890')], ); });