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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
282 lines · 8.6 KB Raw History
import { cborEncodeDagCbor, cidToString, createCid } from '@pdsjs/core/repo';
import { describe, expect, it } from 'vitest';
import { createQueryEngine, fieldAt } from '../src/query.js';

const DID = 'did:plc:querytest';
const DEFS = 'dev.pdsjs.query.def';

/**
 * In-memory actorStorage over a mutable record set. Records are stored as
 * DAG-CBOR bytes, the same shape the real storage returns.
 * @param {Map<string, {value: unknown}>} store - uri -> record value
 */
function storageOver(store) {
  const encoded = (/** @type {string} */ uri) => {
    const entry = store.get(uri);
    if (!entry) return null;
    const bytes = cborEncodeDagCbor(entry.value);
    return { bytes, cidOf: createCid(bytes) };
  };
  return /** @type {any} */ ({
    listRecords: async (
      /** @type {string} */ collection,
      /** @type {string|null} */ cursor,
      /** @type {number} */ limit,
    ) => {
      const uris = [...store.keys()]
        .filter((uri) => uri.includes(`/${collection}/`))
        .sort();
      const start = cursor ? Number(cursor) : 0;
      const page = uris.slice(start, start + limit);
      const records = [];
      for (const uri of page) {
        const row = encoded(uri);
        if (!row) continue;
        records.push({
          uri,
          cid: cidToString(await row.cidOf),
          value: row.bytes,
        });
      }
      const next = start + page.length;
      return { records, cursor: next < uris.length ? String(next) : null };
    },
    getRecord: async (/** @type {string} */ uri) => {
      const row = encoded(uri);
      if (!row) return null;
      return { cid: cidToString(await row.cidOf), value: row.bytes };
    },
  });
}

/** @param {Record<string, unknown>} defValue */
function withDef(defValue, extra = new Map()) {
  const store = new Map(extra);
  store.set(`at://${DID}/${DEFS}/test`, {
    value: { $type: DEFS, createdAt: '2026-08-10T00:00:00.000Z', ...defValue },
  });
  return store;
}

/** @param {Map<string, {value: unknown}>} store */
function engineOver(store) {
  return createQueryEngine({
    actorStorage: storageOver(store),
    getDid: async () => DID,
  });
}

/** @param {ReturnType<typeof createQueryEngine>} engine @param {string} qs */
async function run(engine, qs) {
  const url = new URL(`https://pds.example.com/xrpc/dev.pdsjs.query.run${qs}`);
  const handler = engine.routes['/xrpc/dev.pdsjs.query.run'].handler;
  const response = await handler(new Request(url), url);
  return { status: response.status, body: await response.json() };
}

/** Three notes with staggered TIDs and scores. */
function noteRecords() {
  const store = new Map();
  const notes = [
    { rkey: '3aaa', text: 'first', score: 3 },
    { rkey: '3bbb', text: 'second', score: 1 },
    { rkey: '3ccc', text: 'third', score: 2 },
  ];
  for (const note of notes) {
    store.set(`at://${DID}/com.example.note/${note.rkey}`, {
      value: {
        $type: 'com.example.note',
        text: note.text,
        meta: { score: note.score },
      },
    });
  }
  return store;
}

describe('fieldAt', () => {
  it('walks dot paths and returns undefined off the map', () => {
    expect(fieldAt({ a: { b: 7 } }, 'a.b')).toBe(7);
    expect(fieldAt({ a: 1 }, 'a.b')).toBeUndefined();
    expect(fieldAt(null, 'a')).toBeUndefined();
  });
});

describe('dev.pdsjs.query.run', () => {
  it('requires a query name and 404s unknown ones', async () => {
    const engine = engineOver(withDef({ collection: 'com.example.note' }));
    expect((await run(engine, '')).status).toBe(400);
    expect((await run(engine, '?query=nope')).status).toBe(404);
  });

  it('returns a whole collection newest-first by default', async () => {
    const engine = engineOver(
      withDef({ collection: 'com.example.note' }, noteRecords()),
    );
    const { status, body } = await run(engine, '?query=test');
    expect(status).toBe(200);
    expect(body.records.map((/** @type {any} */ r) => r.value.text)).toEqual([
      'third',
      'second',
      'first',
    ]);
    expect(body.cursor).toBeUndefined();
  });

  it('sorts by a dot-path field in either direction', async () => {
    const asc = engineOver(
      withDef(
        {
          collection: 'com.example.note',
          sort: { field: 'meta.score', direction: 'asc' },
        },
        noteRecords(),
      ),
    );
    const { body } = await run(asc, '?query=test');
    expect(
      body.records.map((/** @type {any} */ r) => r.value.meta.score),
    ).toEqual([1, 2, 3]);

    const desc = engineOver(
      withDef(
        {
          collection: 'com.example.note',
          sort: { field: 'meta.score', direction: 'desc' },
        },
        noteRecords(),
      ),
    );
    const result = await run(desc, '?query=test');
    expect(
      result.body.records.map((/** @type {any} */ r) => r.value.meta.score),
    ).toEqual([3, 2, 1]);
  });

  it('filters on where equality over dot paths', async () => {
    const engine = engineOver(
      withDef(
        { collection: 'com.example.note', where: { 'meta.score': 2 } },
        noteRecords(),
      ),
    );
    const { body } = await run(engine, '?query=test');
    expect(body.records).toHaveLength(1);
    expect(body.records[0].value.text).toBe('third');
  });

  it('pages with limit and an offset cursor', async () => {
    const engine = engineOver(
      withDef({ collection: 'com.example.note' }, noteRecords()),
    );
    const first = await run(engine, '?query=test&limit=2');
    expect(first.body.records).toHaveLength(2);
    expect(first.body.cursor).toBe('2');
    const second = await run(engine, '?query=test&limit=2&cursor=2');
    expect(second.body.records).toHaveLength(1);
    expect(second.body.cursor).toBeUndefined();
  });

  it('rejects a definition without a collection', async () => {
    const engine = engineOver(withDef({}));
    expect((await run(engine, '?query=test')).status).toBe(400);
  });
});

describe('onCommit maintenance', () => {
  it('applies creates, updates, and deletes to a loaded collection', async () => {
    const store = withDef({ collection: 'com.example.note' }, noteRecords());
    const engine = engineOver(store);
    await run(engine, '?query=test');

    const uri = `at://${DID}/com.example.note/3ddd`;
    store.set(uri, {
      value: { $type: 'com.example.note', text: 'fourth', meta: { score: 9 } },
    });
    await engine.onCommit({
      did: DID,
      seq: 1,
      rev: 'rev1',
      ops: [{ action: 'create', uri, cid: 'bafycid' }],
    });
    let result = await run(engine, '?query=test');
    expect(result.body.records[0].value.text).toBe('fourth');

    store.set(uri, {
      value: { $type: 'com.example.note', text: 'fourth-edited', meta: {} },
    });
    await engine.onCommit({
      did: DID,
      seq: 2,
      rev: 'rev2',
      ops: [{ action: 'update', uri, cid: 'bafycid2' }],
    });
    result = await run(engine, '?query=test');
    expect(result.body.records[0].value.text).toBe('fourth-edited');

    store.delete(uri);
    await engine.onCommit({
      did: DID,
      seq: 3,
      rev: 'rev3',
      ops: [{ action: 'delete', uri, cid: null }],
    });
    result = await run(engine, '?query=test');
    expect(result.body.records).toHaveLength(3);
  });

  it('leaves collections nobody queried alone', async () => {
    const store = withDef({ collection: 'com.example.note' }, noteRecords());
    const storage = storageOver(store);
    let reads = 0;
    const counted = /** @type {any} */ ({
      ...storage,
      getRecord: (/** @type {string} */ uri) => {
        reads += 1;
        return storage.getRecord(uri);
      },
    });
    const engine = createQueryEngine({
      actorStorage: counted,
      getDid: async () => DID,
    });
    await engine.onCommit({
      did: DID,
      seq: 1,
      rev: 'rev1',
      ops: [
        {
          action: 'create',
          uri: `at://${DID}/com.example.note/3zzz`,
          cid: 'bafycid',
        },
      ],
    });
    expect(reads).toBe(0);
  });

  it('makes a def written after startup queryable at once', async () => {
    const store = withDef({ collection: 'com.example.note' }, noteRecords());
    const engine = engineOver(store);
    await run(engine, '?query=test');

    const uri = `at://${DID}/${DEFS}/scores`;
    store.set(uri, {
      value: {
        $type: DEFS,
        collection: 'com.example.note',
        sort: { field: 'meta.score', direction: 'asc' },
        createdAt: '2026-08-10T00:00:00.000Z',
      },
    });
    await engine.onCommit({
      did: DID,
      seq: 4,
      rev: 'rev4',
      ops: [{ action: 'create', uri, cid: 'bafydef' }],
    });
    const { status, body } = await run(engine, '?query=scores');
    expect(status).toBe(200);
    expect(body.records[0].value.meta.score).toBe(1);
  });
});