UNPKG

@instantdb/core

Version:

Instant's core local abstraction

532 lines (496 loc) 14.1 kB
import { test, expect } from 'vitest'; import zenecaAttrs from './data/zeneca/attrs.json'; import zenecaTriples from './data/zeneca/triples.json'; import { createStore, transact, allMapValues, toJSON, fromJSON, } from '../../src/store'; import query from '../../src/instaql'; import uuid from '../../src/utils/uuid'; import { tx } from '../../src/instatx'; import * as instaml from '../../src/instaml'; import * as datalog from '../../src/datalog'; const zenecaIdToAttr = zenecaAttrs.reduce((res, x) => { res[x.id] = x; return res; }, {}); const store = createStore(zenecaIdToAttr, zenecaTriples); function checkIndexIntegrity(store) { const tripleSort = (a, b) => { const [e_a, aid_a, v_a, t_a] = a; const [e_b, aid_b, v_b, t_b] = b; const e_compare = e_a.localeCompare(e_b); if (e_compare !== 0) { return e_compare; } const a_compare = aid_a.localeCompare(aid_b); if (a_compare !== 0) { return a_compare; } const v_compare = JSON.stringify(v_a).localeCompare(JSON.stringify(v_b)); if (v_compare !== 0) { return v_compare; } return t_a - t_b; }; const eavTriples = allMapValues(store.eav, 3).sort(tripleSort); const aevTriples = allMapValues(store.aev, 3).sort(tripleSort); const vaeTriples = allMapValues(store.vae, 3); // Check eav and aev have all the same values expect(eavTriples).toEqual(aevTriples); // Check vae doesn't have extra triples for (const triple of vaeTriples) { const [e, a, v] = triple; expect(store.eav.get(e)?.get(a)?.get(v)).toEqual(triple); } // Check vae has all of the triples it should have for (const triple of eavTriples) { const [e, a, v] = triple; const attr = store.attrs[a]; if (attr['value-type'] === 'ref') { expect(store.vae.get(v)?.get(a)?.get(e)).toEqual(triple); } } } test('simple add', () => { const id = uuid(); const chunk = tx.users[id].update({ handle: 'bobby' }); const txSteps = instaml.transform({ attrs: store.attrs }, chunk); const newStore = transact(store, txSteps); expect( query({ store: newStore }, { users: {} }).data.users.map((x) => x.handle), ).contains('bobby'); checkIndexIntegrity(newStore); }); test('cardinality-one add', () => { const id = uuid(); const chunk = tx.users[id] .update({ handle: 'bobby' }) .update({ handle: 'bob' }); const txSteps = instaml.transform({ attrs: store.attrs }, chunk); const newStore = transact(store, txSteps); const ret = datalog .query(newStore, { find: ['?v'], where: [[id, '?attr', '?v']], }) .flatMap((vec) => vec[0]); expect(ret).contains('bob'); expect(ret).not.contains('bobby'); checkIndexIntegrity(newStore); }); test('link/unlink', () => { const bookshelfId = uuid(); const userId = uuid(); const userChunk = tx.users[userId] .update({ handle: 'bobby' }) .link({ bookshelves: bookshelfId }); const bookshelfChunk = tx.bookshelves[bookshelfId].update({ name: 'my books', }); const txSteps = instaml.transform({ attrs: store.attrs }, [ userChunk, bookshelfChunk, ]); const newStore = transact(store, txSteps); expect( query( { store: newStore }, { users: { $: { where: { handle: 'bobby' } }, bookshelves: {}, }, }, ).data.users.map((x) => [x.handle, x.bookshelves.map((x) => x.name)]), ).toEqual([['bobby', ['my books']]]); checkIndexIntegrity(newStore); const secondBookshelfId = uuid(); const secondBookshelfChunk = tx.bookshelves[secondBookshelfId].update({ name: 'my second books', }); const unlinkFirstChunk = tx.users[userId] .unlink({ bookshelves: bookshelfId, }) .link({ bookshelves: secondBookshelfId }); const secondTxSteps = instaml.transform({ attrs: newStore.attrs }, [ unlinkFirstChunk, secondBookshelfChunk, ]); const secondStore = transact(newStore, secondTxSteps); expect( query( { store: secondStore }, { users: { $: { where: { handle: 'bobby' } }, bookshelves: {}, }, }, ).data.users.map((x) => [x.handle, x.bookshelves.map((x) => x.name)]), ).toEqual([['bobby', ['my second books']]]); checkIndexIntegrity(secondStore); }); test('link/unlink multi', () => { const bookshelfId1 = uuid(); const bookshelfId2 = uuid(); const userId = uuid(); const userChunk = tx.users[userId] .update({ handle: 'bobby' }) .link({ bookshelves: [bookshelfId1, bookshelfId2] }); const bookshelf1Chunk = tx.bookshelves[bookshelfId1].update({ name: 'my books 1', }); const bookshelf2Chunk = tx.bookshelves[bookshelfId2].update({ name: 'my books 2', }); const txSteps = instaml.transform({ attrs: store.attrs }, [ userChunk, bookshelf1Chunk, bookshelf2Chunk, ]); const newStore = transact(store, txSteps); expect( query( { store: newStore }, { users: { $: { where: { handle: 'bobby' } }, bookshelves: {}, }, }, ).data.users.map((x) => [x.handle, x.bookshelves.map((x) => x.name)]), ).toEqual([['bobby', ['my books 1', 'my books 2']]]); checkIndexIntegrity(newStore); const bookshelfId3 = uuid(); const bookshelf3Chunk = tx.bookshelves[bookshelfId3].update({ name: 'my books 3', }); const unlinkChunk = tx.users[userId] .unlink({ bookshelves: [bookshelfId1, bookshelfId2], }) .link({ bookshelves: bookshelfId3 }); const secondTxSteps = instaml.transform({ attrs: newStore.attrs }, [ unlinkChunk, bookshelf3Chunk, ]); const secondStore = transact(newStore, secondTxSteps); expect( query( { store: secondStore }, { users: { $: { where: { handle: 'bobby' } }, bookshelves: {}, }, }, ).data.users.map((x) => [x.handle, x.bookshelves.map((x) => x.name)]), ).toEqual([['bobby', ['my books 3']]]); checkIndexIntegrity(secondStore); }); test('delete entity', () => { const bookshelfId = uuid(); const userId = uuid(); const userChunk = tx.users[userId] .update({ handle: 'bobby' }) .link({ bookshelves: bookshelfId }); const bookshelfChunk = tx.bookshelves[bookshelfId].update({ name: 'my books', }); const txSteps = instaml.transform({ attrs: store.attrs }, [ userChunk, bookshelfChunk, ]); const newStore = transact(store, txSteps); checkIndexIntegrity(newStore); const retOne = datalog .query(newStore, { find: ['?v'], where: [[bookshelfId, '?attr', '?v']], }) .flatMap((vec) => vec[0]); const retTwo = datalog .query(newStore, { find: ['?v'], where: [['?v', '?attr', bookshelfId]], }) .flatMap((vec) => vec[0]); expect(retOne).contains('my books'); expect(retTwo).contains(userId); const txStepsTwo = instaml.transform( { attrs: newStore.attrs }, tx.bookshelves[bookshelfId].delete(), ); const newStoreTwo = transact(newStore, txStepsTwo); const retThree = datalog .query(newStoreTwo, { find: ['?v'], where: [[bookshelfId, '?attr', '?v']], }) .flatMap((vec) => vec[0]); const retFour = datalog .query(newStoreTwo, { find: ['?v'], where: [['?v', '?attr', bookshelfId]], }) .flatMap((vec) => vec[0]); expect(retThree).toEqual([]); expect(retFour).toEqual([]); checkIndexIntegrity(newStoreTwo); }); test('on-delete cascade', () => { const book1 = uuid(); const book2 = uuid(); const book3 = uuid(); const chunk1 = tx.books[book1].update({ title: 'book1', description: 'series', }); const chunk2 = tx.books[book2] .update({ title: 'book2', description: 'series' }) .link({ prequel: book1 }); const chunk3 = tx.books[book3] .update({ title: 'book3', description: 'series' }) .link({ prequel: book2 }); const txSteps = instaml.transform({ attrs: store.attrs }, [ chunk1, chunk2, chunk3, ]); const newStore = transact(store, txSteps); checkIndexIntegrity(newStore); expect( query( { store: newStore }, { books: { $: { where: { description: 'series' } } } }, ).data.books.map((x) => x.title), ).toEqual(['book1', 'book2', 'book3']); const txStepsTwo = instaml.transform( { attrs: newStore.attrs }, tx.books[book1].delete(), ); const newStoreTwo = transact(newStore, txStepsTwo); expect( query( { store: newStoreTwo }, { books: { $: { where: { description: 'series' } } } }, ).data.books.map((x) => x.title), ).toEqual([]); }); test('on-delete-reverse cascade', () => { const book1 = uuid(); const book2 = uuid(); const book3 = uuid(); const chunk2 = tx.books[book2].update({ title: 'book2', description: 'series', }); const chunk3 = tx.books[book3].update({ title: 'book3', description: 'series', }); const chunk1 = tx.books[book1] .update({ title: 'book1', description: 'series', }) .link({ next: [book2, book3] }); const txSteps = instaml.transform({ attrs: store.attrs }, [ chunk2, chunk3, chunk1, ]); const newStore = transact(store, txSteps); checkIndexIntegrity(newStore); expect( query( { store: newStore }, { books: { $: { where: { description: 'series' } } } }, ).data.books.map((x) => x.title), ).toEqual(['book2', 'book3', 'book1']); const txStepsTwo = instaml.transform( { attrs: newStore.attrs }, tx.books[book1].delete(), ); const newStoreTwo = transact(newStore, txStepsTwo); expect( query( { store: newStoreTwo }, { books: { $: { where: { description: 'series' } } } }, ).data.books.map((x) => x.title), ).toEqual([]); }); test('new attrs', () => { const colorId = uuid(); const userId = uuid(); const userChunk = tx.users[userId] .update({ handle: 'bobby' }) .link({ colors: colorId }); const colorChunk = tx.colors[colorId].update({ name: 'red' }); const txSteps = instaml.transform({ attrs: store.attrs }, [ userChunk, colorChunk, ]); const newStore = transact(store, txSteps); expect( query( { store: newStore }, { users: { $: { where: { handle: 'bobby' } }, colors: {}, }, }, ).data.users.map((x) => [x.handle, x.colors.map((x) => x.name)]), ).toEqual([['bobby', ['red']]]); checkIndexIntegrity(newStore); }); test('delete attr', () => { expect( query({ store }, { users: {} }).data.users.map((x) => [ x.handle, x.fullName, ]), ).toEqual([ ['joe', 'Joe Averbukh'], ['alex', 'Alex'], ['stopa', 'Stepan Parunashvili'], ['nicolegf', 'Nicole'], ]); const fullNameAttr = instaml.getAttrByFwdIdentName( store.attrs, 'users', 'fullName', ); const newStore = transact(store, [['delete-attr', fullNameAttr.id]]); expect( query({ store: newStore }, { users: {} }).data.users.map((x) => [ x.handle, x.fullName, ]), ).toEqual([ ['joe', undefined], ['alex', undefined], ['stopa', undefined], ['nicolegf', undefined], ]); checkIndexIntegrity(newStore); }); test('update attr', () => { expect( query({ store }, { users: {} }).data.users.map((x) => [ x.handle, x.fullName, ]), ).toEqual([ ['joe', 'Joe Averbukh'], ['alex', 'Alex'], ['stopa', 'Stepan Parunashvili'], ['nicolegf', 'Nicole'], ]); const fullNameAttr = instaml.getAttrByFwdIdentName( store.attrs, 'users', 'fullName', ); const fwdIdent = fullNameAttr['forward-identity']; const newStore = transact(store, [ [ 'update-attr', { id: fullNameAttr.id, 'forward-identity': [fwdIdent[0], 'users', 'fullNamez'], }, ], ]); expect( query({ store: newStore }, { users: {} }).data.users.map((x) => [ x.handle, x.fullNamez, ]), ).toEqual([ ['joe', 'Joe Averbukh'], ['alex', 'Alex'], ['stopa', 'Stepan Parunashvili'], ['nicolegf', 'Nicole'], ]); }); test('JSON serialization round-trips', () => { const newStore = fromJSON(toJSON(store)); expect(store).toEqual(newStore); }); test('ruleParams no-ops', () => { const id = uuid(); const chunk = tx.users[id] .ruleParams({ guestId: 'bobby' }) .update({ handle: 'bobby' }); const txSteps = instaml.transform({ attrs: store.attrs }, chunk); const newStore = transact(store, txSteps); expect( query({ store: newStore }, { users: {} }).data.users.map((x) => x.handle), ).contains('bobby'); checkIndexIntegrity(newStore); }); test('deepMerge', () => { const gameId = uuid(); const gameStore = transact( store, instaml.transform( { attrs: store.attrs }, tx.games[gameId].update({ state: { score: 100, playerStats: { health: 50, mana: 30, ambitions: { win: true } }, inventory: ['sword', 'potion'], locations: ['forest', 'castle'], level: 2, }, }), ), ); const updatedStore = transact( gameStore, instaml.transform( { attrs: gameStore.attrs }, tx.games[gameId].merge({ state: { // Objects update deeply playerStats: { mana: 40, stamina: 20, ambitions: { acquireWisdom: true, find: ['love'] }, }, // arrays overwrite inventory: ['shield'], // null removes the key score: null, // undefined is ignored level: undefined, // undefined is kept in arrays locations: ['forest', undefined, 'castle'], }, }), ), ); const updatedGame = query( { store: updatedStore }, { games: { $: { where: { id: gameId } } } }, ).data.games[0]; expect(updatedGame.state).toEqual({ playerStats: { health: 50, mana: 40, stamina: 20, ambitions: { win: true, acquireWisdom: true, find: ['love'] }, }, level: 2, inventory: ['shield'], locations: ['forest', undefined, 'castle'], }); checkIndexIntegrity(updatedGame); });