import Database from 'better-sqlite3'; import { describe, expect, it } from 'vitest'; import { createAppViewStore, linksOf } from '../src/store.js'; const COLLECTION = 'dev.pdsjs.git.pull'; const OWNER = 'did:plc:owner'; const SUBJECT = `at://${OWNER}/dev.pdsjs.git.repo/proj`; /** * @param {Partial>} [overrides] * @param {string} [rkey] */ const pullWrite = (overrides = {}, rkey = '3kpull') => ({ did: 'did:plc:stranger', collection: COLLECTION, rkey, cid: 'bafypull', uri: `at://did:plc:stranger/${COLLECTION}/${rkey}`, seq: 1, value: { subject: { uri: SUBJECT }, title: 'Fix the thing' }, ...overrides, }); describe('createAppViewStore', () => { it('holds a record whole and answers it back by uri', () => { const store = createAppViewStore(new Database(':memory:')); store.putRecord(pullWrite()); const row = store.getRecord( 'at://did:plc:stranger/dev.pdsjs.git.pull/3kpull', ); expect(row).toEqual({ uri: 'at://did:plc:stranger/dev.pdsjs.git.pull/3kpull', did: 'did:plc:stranger', collection: COLLECTION, rkey: '3kpull', cid: 'bafypull', value: { subject: { uri: SUBJECT }, title: 'Fix the thing' }, indexedAt: expect.any(String), rowid: expect.any(Number), }); expect(store.getRecord('at://did:plc:x/app/nope/1')).toBe(null); }); it('replaces a record and keeps no stale links', () => { const store = createAppViewStore(new Database(':memory:')); store.putRecord(pullWrite()); store.putRecord( pullWrite({ cid: 'bafypull2', value: { subject: { uri: 'at://did:plc:other/dev.pdsjs.git.repo/proj' }, }, }), ); const row = store.getRecord( 'at://did:plc:stranger/dev.pdsjs.git.pull/3kpull', ); expect(row?.cid).toBe('bafypull2'); expect(store.about(SUBJECT, COLLECTION)).toEqual([]); expect( store.about('at://did:plc:other/dev.pdsjs.git.repo/proj', COLLECTION), ).toHaveLength(1); }); it('removes the record and its links on delete', () => { const store = createAppViewStore(new Database(':memory:')); store.putRecord(pullWrite()); store.deleteRecord('at://did:plc:stranger/dev.pdsjs.git.pull/3kpull'); expect( store.getRecord('at://did:plc:stranger/dev.pdsjs.git.pull/3kpull'), ).toBe(null); expect(store.about(SUBJECT, COLLECTION)).toEqual([]); }); it('answers the discovery question: records naming a subject at a path', () => { const store = createAppViewStore(new Database(':memory:')); store.putRecord(pullWrite()); store.putRecord(pullWrite({ cid: 'bafy2' }, '3ksecond')); const found = store.about(SUBJECT, COLLECTION); expect(found).toHaveLength(2); expect(found[0].collection).toBe(COLLECTION); // A different collection answers nothing. expect(store.about(SUBJECT, 'dev.pdsjs.git.review')).toEqual([]); // A different path answers nothing. expect(store.about(SUBJECT, COLLECTION, 'upstream.uri')).toEqual([]); }); it('lists a collection newest first and pages by insertion order', () => { const store = createAppViewStore(new Database(':memory:')); store.putRecord(pullWrite({}, 'a')); store.putRecord(pullWrite({}, 'b')); expect(store.listRecords(COLLECTION).map((row) => row.rkey)).toEqual([ 'b', 'a', ]); const newest = store.listRecords(COLLECTION, 1)[0]; expect(newest.rkey).toBe('b'); expect( store.listRecords(COLLECTION, 100, newest.rowid).map((row) => row.rkey), ).toEqual(['a']); expect( store.listRecords( COLLECTION, 100, store.listRecords(COLLECTION)[0].rowid, ), ).toHaveLength(1); }); it('keeps handles and clears an account whole', () => { const store = createAppViewStore(new Database(':memory:')); store.setIdentity('did:plc:stranger', 'stranger.example.com'); expect(store.handleOf('did:plc:stranger')).toBe('stranger.example.com'); expect(store.handleOf('did:plc:nobody')).toBe(null); store.putRecord(pullWrite()); store.putRecord( pullWrite( { did: 'did:plc:other', uri: 'at://did:plc:other/dev.pdsjs.git.pull/3kpull', }, '3kother', ), ); store.clearAccount('did:plc:stranger'); // Only the stranger's rows go; the other account's record stays. expect( store.getRecord('at://did:plc:stranger/dev.pdsjs.git.pull/3kpull'), ).toBe(null); const left = store.about(SUBJECT, COLLECTION); expect(left).toHaveLength(1); expect(left[0].did).toBe('did:plc:other'); // The handle stays; an identity event is not the account's data. expect(store.handleOf('did:plc:stranger')).toBe('stranger.example.com'); }); it('stores cursors as text and reads numbers back', () => { const store = createAppViewStore(new Database(':memory:')); expect(store.cursor('jetstream')).toBe(undefined); store.setCursor('jetstream', 4242); expect(store.cursor('jetstream')).toBe(4242); store.setTextCursor('backfill:dev.pdsjs.git.pull', 'page-two'); expect(store.textCursor('backfill:dev.pdsjs.git.pull')).toBe('page-two'); // A non-numeric cursor is not a number for the seq readers. store.setCursor('backfill:x', 1); expect(store.textCursor('backfill:x')).toBe('1'); }); it('links references found anywhere in the record', () => { const store = createAppViewStore(new Database(':memory:')); store.putRecord( pullWrite({ value: { subject: { uri: SUBJECT }, replies: [{ uri: 'at://did:plc:x/app.post/1' }], mentions: ['at://did:plc:y/app.post/2'], upstream: SUBJECT, bare: 'at://did:plc:z', plain: 'not an at uri', }, }), ); const found = store.about(SUBJECT, COLLECTION); expect(found).toHaveLength(1); // A list folded its positions into one path; each subject is findable // through the field it was named in. expect( store.about('at://did:plc:x/app.post/1', COLLECTION, 'replies.uri'), ).toHaveLength(1); expect( store.about('at://did:plc:y/app.post/2', COLLECTION, 'mentions'), ).toHaveLength(1); expect(store.about('at://did:plc:z', COLLECTION)).toEqual([]); }); it('does not treat a blob $link or non-string value as a link', () => { expect( linksOf({ subject: { uri: SUBJECT }, part: { $link: 'bafyblob' } }), ).toEqual([{ path: 'subject.uri', subject: SUBJECT }]); expect(linksOf(null)).toEqual([]); }); });