an app to share curated trails
sidetrail.app
1import { readFileSync } from "node:fs";
2import { Lexicons, jsonToLex, type LexiconDoc } from "@atproto/lexicon";
3
4// Validate against the lexicon JSON documents directly (the same source the
5// generated lib/lexicons code is built from). PDSes don't reliably enforce
6// third-party lexicons, so validating is the indexer's responsibility.
7const loadDoc = (path: string): LexiconDoc =>
8 JSON.parse(readFileSync(new URL(`../../lexicons/${path}`, import.meta.url), "utf8"));
9
10const lexicons = new Lexicons([
11 loadDoc("app/sidetrail/trail.json"),
12 loadDoc("app/sidetrail/walk.json"),
13 loadDoc("app/sidetrail/completion.json"),
14 loadDoc("com/atproto/repo/strongRef.json"),
15]);
16
17export const INDEXED_COLLECTIONS = [
18 "app.sidetrail.trail",
19 "app.sidetrail.walk",
20 "app.sidetrail.completion",
21] as const;
22
23export type IndexedCollection = (typeof INDEXED_COLLECTIONS)[number];
24
25export function validateRecord(
26 collection: IndexedCollection,
27 record: unknown,
28): { success: true } | { success: false; reason: string } {
29 // Records arrive as JSON (jetstream, listRecords, jsonb storage), where CID
30 // links are {"$link": ...} objects. The validator expects lex values, so
31 // convert first; a record that can't be converted (e.g. malformed CID) is invalid.
32 let lexValue: unknown;
33 try {
34 lexValue = jsonToLex(record);
35 } catch (err) {
36 return { success: false, reason: `unparseable as lex: ${(err as Error).message}` };
37 }
38 const result = lexicons.validate(collection, lexValue);
39 if (result.success) return { success: true };
40 return { success: false, reason: result.error.message };
41}