diff --git a/src/generate.ts b/src/generate.ts index dfc838a..c56fa6b 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -9,6 +9,38 @@ import { writeFileSync, mkdirSync, rmSync, existsSync, readFileSync, readdirSync import { join } from "path"; import type { ContrailConfig } from "./core/types"; +/** Return the sorted list of XRPC method NSIDs (queries + procedures) in a + * generated lexicon set — the same list that ends up in `.permissionSet`'s + * `lxm`. Use when the permission set isn't published yet and you need the + * scoped method list inline (e.g. in an OAuth client config). + * + * Filters `defs.main.type === "query" | "procedure"`. Sorted alphabetically + * so the output is stable across runs. */ +export function extractXrpcMethods(generated: Record): string[] { + const methods: string[] = []; + for (const [nsid, doc] of Object.entries(generated)) { + const mainType = (doc as { defs?: { main?: { type?: string } } })?.defs?.main?.type; + if (mainType === "query" || mainType === "procedure") methods.push(nsid); + } + return methods.sort(); +} + +/** Generate lexicons in-memory and return the sorted method NSID list. + * Convenience wrapper over `generateLexicons` + `extractXrpcMethods` for + * callers that don't care about the full lexicon map. */ +export function listXrpcMethods( + config: ContrailConfig, + options: { rootDir: string; lexiconDirs?: string[] } +): string[] { + const generated = generateLexicons({ + config, + rootDir: options.rootDir, + lexiconDirs: options.lexiconDirs, + quiet: true, + }); + return extractXrpcMethods(generated); +} + export interface GenerateOptions { config: ContrailConfig; /** Root project directory (for finding lexicon source files). */ diff --git a/tests/generate.test.ts b/tests/generate.test.ts index 8ab7a61..2a3456b 100644 --- a/tests/generate.test.ts +++ b/tests/generate.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeAll } from "vitest"; import { join } from "path"; -import { generateLexicons } from "../src/generate"; +import { generateLexicons, extractXrpcMethods, listXrpcMethods } from "../src/generate"; import type { ContrailConfig } from "../src/core/types"; const ROOT_DIR = join(__dirname, ".."); @@ -219,3 +219,39 @@ describe("search: no searchable field configured", () => { expect(params.search).toBeUndefined(); }); }); + +describe("extractXrpcMethods / listXrpcMethods", () => { + it("extracts only queries and procedures from a generated lexicon map", () => { + const lexicons = generate(BASIC_CONFIG); + const methods = extractXrpcMethods(lexicons); + // Sorted; includes admin + profile + listRecords/getRecord for the collection. + expect(methods).toEqual([...methods].sort()); + expect(methods).toContain("test.app.post.listRecords"); + expect(methods).toContain("test.app.post.getRecord"); + expect(methods).toContain("test.app.getProfile"); + // Does not include non-method defs (e.g. `defs`, records, permission-set). + expect(methods).not.toContain("test.app.permissionSet"); + }); + + it("listXrpcMethods matches the permissionSet lxm list for the same config", () => { + const methods = listXrpcMethods(BASIC_CONFIG, { rootDir: ROOT_DIR, lexiconDirs: [] }); + const lexicons = generate(BASIC_CONFIG); + const ps = (lexicons["test.app.permissionSet"] as any).defs.main.permissions[0]; + expect(ps.lxm).toEqual(methods); + }); + + it("includes realtime + community + spaces endpoints when those modules are enabled", () => { + const config: ContrailConfig = { + namespace: "test.comm", + collections: { message: { collection: "app.event.message" } }, + spaces: { type: "tools.atmo.event.space", serviceDid: "did:web:test.example#svc" }, + community: { masterKey: new Uint8Array(32).fill(1) }, + realtime: { ticketSecret: new Uint8Array(32).fill(2) }, + }; + const methods = listXrpcMethods(config, { rootDir: ROOT_DIR, lexiconDirs: [] }); + expect(methods).toContain("test.comm.space.createSpace"); + expect(methods).toContain("test.comm.community.adopt"); + expect(methods).toContain("test.comm.realtime.ticket"); + expect(methods).toContain("test.comm.realtime.subscribe"); + }); +});