chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
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<Record<string, unknown>>} [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([]);
});
});