~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
183 lines · 6.4 KB Raw History
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([]);
  });
});