@atproto/lex-cli
Version:
TypeScript codegen tool for atproto Lexicon schemas
480 lines • 17.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.genClientApi = genClientApi;
const ts_morph_1 = require("ts-morph");
const lexicon_1 = require("@atproto/lexicon");
const syntax_1 = require("@atproto/syntax");
const common_1 = require("./common");
const lex_gen_1 = require("./lex-gen");
const util_1 = require("./util");
const ATP_METHODS = {
list: 'com.atproto.repo.listRecords',
get: 'com.atproto.repo.getRecord',
create: 'com.atproto.repo.createRecord',
put: 'com.atproto.repo.putRecord',
delete: 'com.atproto.repo.deleteRecord',
};
async function genClientApi(lexiconDocs) {
const project = new ts_morph_1.Project({
useInMemoryFileSystem: true,
manipulationSettings: { indentationText: ts_morph_1.IndentationText.TwoSpaces },
});
const api = { files: [] };
const lexicons = new lexicon_1.Lexicons(lexiconDocs);
const nsidTree = (0, util_1.lexiconsToDefTree)(lexiconDocs);
const nsidTokens = (0, util_1.schemasToNsidTokens)(lexiconDocs);
for (const lexiconDoc of lexiconDocs) {
api.files.push(await lexiconTs(project, lexicons, lexiconDoc));
}
api.files.push(await (0, common_1.utilTs)(project));
api.files.push(await (0, common_1.lexiconsTs)(project, lexiconDocs));
api.files.push(await indexTs(project, lexiconDocs, nsidTree, nsidTokens));
return api;
}
const indexTs = (project, lexiconDocs, nsidTree, nsidTokens) => (0, common_1.gen)(project, '/index.ts', async (file) => {
//= import { XrpcClient, type FetchHandler, type FetchHandlerOptions } from '@atproto/xrpc'
const xrpcImport = file.addImportDeclaration({
moduleSpecifier: '@atproto/xrpc',
});
xrpcImport.addNamedImports([
{ name: 'XrpcClient' },
{ name: 'FetchHandler', isTypeOnly: true },
{ name: 'FetchHandlerOptions', isTypeOnly: true },
]);
//= import {schemas} from './lexicons.js'
file
.addImportDeclaration({ moduleSpecifier: './lexicons.js' })
.addNamedImports([{ name: 'schemas' }]);
//= import {CID} from 'multiformats/cid'
file
.addImportDeclaration({
moduleSpecifier: 'multiformats/cid',
})
.addNamedImports([{ name: 'CID' }]);
//= import { type OmitKey, type Un$Typed } from './util.js'
file
.addImportDeclaration({ moduleSpecifier: `./util.js` })
.addNamedImports([
{ name: 'OmitKey', isTypeOnly: true },
{ name: 'Un$Typed', isTypeOnly: true },
]);
// generate type imports and re-exports
for (const lexicon of lexiconDocs) {
const moduleSpecifier = `./types/${lexicon.id.split('.').join('/')}.js`;
file
.addImportDeclaration({ moduleSpecifier })
.setNamespaceImport((0, util_1.toTitleCase)(lexicon.id));
file
.addExportDeclaration({ moduleSpecifier })
.setNamespaceExport((0, util_1.toTitleCase)(lexicon.id));
}
// generate token enums
for (const nsidAuthority in nsidTokens) {
// export const {THE_AUTHORITY} = {
// {Name}: "{authority.the.name}"
// }
file.addVariableStatement({
isExported: true,
declarationKind: ts_morph_1.VariableDeclarationKind.Const,
declarations: [
{
name: (0, util_1.toScreamingSnakeCase)(nsidAuthority),
initializer: [
'{',
...nsidTokens[nsidAuthority].map((nsidName) => `${(0, util_1.toTitleCase)(nsidName)}: "${nsidAuthority}.${nsidName}",`),
'}',
].join('\n'),
},
],
});
}
//= export class AtpBaseClient {...}
const clientCls = file.addClass({
name: 'AtpBaseClient',
isExported: true,
extends: 'XrpcClient',
});
for (const ns of nsidTree) {
//= ns: NS
clientCls.addProperty({
name: ns.propName,
type: ns.className,
});
}
//= constructor (options: FetchHandler | FetchHandlerOptions) {
//= super(options, schemas)
//= {namespace declarations}
//= }
clientCls.addConstructor({
parameters: [
{ name: 'options', type: 'FetchHandler | FetchHandlerOptions' },
],
statements: [
'super(options, schemas)',
...nsidTree.map((ns) => `this.${ns.propName} = new ${ns.className}(this)`),
],
});
//= /** @deprecated use `this` instead */
//= get xrpc(): XrpcClient {
//= return this
//= }
clientCls
.addGetAccessor({
name: 'xrpc',
returnType: 'XrpcClient',
statements: ['return this'],
})
.addJsDoc('@deprecated use `this` instead');
// generate classes for the schemas
for (const ns of nsidTree) {
genNamespaceCls(file, ns);
}
});
function genNamespaceCls(file, ns) {
//= export class {ns}NS {...}
const cls = file.addClass({
name: ns.className,
isExported: true,
});
//= _client: XrpcClient
cls.addProperty({
name: '_client',
type: 'XrpcClient',
});
for (const userType of ns.userTypes) {
if (userType.def.type !== 'record') {
continue;
}
//= type: TypeRecord
const name = syntax_1.NSID.parse(userType.nsid).name || '';
cls.addProperty({
name: (0, util_1.toCamelCase)(name),
type: `${(0, util_1.toTitleCase)(userType.nsid)}Record`,
});
}
for (const child of ns.children) {
//= child: ChildNS
cls.addProperty({
name: child.propName,
type: child.className,
});
// recurse
genNamespaceCls(file, child);
}
//= constructor(public client: XrpcClient) {
//= this._client = client
//= {child namespace prop declarations}
//= {record prop declarations}
//= }
cls.addConstructor({
parameters: [
{
name: 'client',
type: 'XrpcClient',
},
],
statements: [
`this._client = client`,
...ns.children.map((ns) => `this.${ns.propName} = new ${ns.className}(client)`),
...ns.userTypes
.filter((ut) => ut.def.type === 'record')
.map((ut) => {
const name = syntax_1.NSID.parse(ut.nsid).name || '';
return `this.${(0, util_1.toCamelCase)(name)} = new ${(0, util_1.toTitleCase)(ut.nsid)}Record(client)`;
}),
],
});
// methods
for (const userType of ns.userTypes) {
if (userType.def.type !== 'query' && userType.def.type !== 'procedure') {
continue;
}
const isGetReq = userType.def.type === 'query';
const moduleName = (0, util_1.toTitleCase)(userType.nsid);
const name = (0, util_1.toCamelCase)(syntax_1.NSID.parse(userType.nsid).name || '');
const method = cls.addMethod({
name,
returnType: `Promise<${moduleName}.Response>`,
});
if (isGetReq) {
method.addParameter({
name: 'params?',
type: `${moduleName}.QueryParams`,
});
}
else if (userType.def.type === 'procedure') {
method.addParameter({
name: 'data?',
type: `${moduleName}.InputSchema`,
});
}
method.addParameter({
name: 'opts?',
type: `${moduleName}.CallOptions`,
});
method.setBodyText([
`return this._client`,
isGetReq
? `.call('${userType.nsid}', params, undefined, opts)`
: `.call('${userType.nsid}', opts?.qp, data, opts)`,
userType.def.errors?.length
? // Only add a catch block if there are custom errors
` .catch((e) => { throw ${moduleName}.toKnownErr(e) })`
: '',
].join('\n'));
}
// record api classes
for (const userType of ns.userTypes) {
if (userType.def.type !== 'record') {
continue;
}
genRecordCls(file, userType.nsid, userType.def);
}
}
function genRecordCls(file, nsid, lexRecord) {
//= export class {type}Record {...}
const cls = file.addClass({
name: `${(0, util_1.toTitleCase)(nsid)}Record`,
isExported: true,
});
//= _client: XrpcClient
cls.addProperty({
name: '_client',
type: 'XrpcClient',
});
//= constructor(client: XrpcClient) {
//= this._client = client
//= }
const cons = cls.addConstructor();
cons.addParameter({
name: 'client',
type: 'XrpcClient',
});
cons.setBodyText(`this._client = client`);
// methods
const typeModule = (0, util_1.toTitleCase)(nsid);
{
//= list()
const method = cls.addMethod({
isAsync: true,
name: 'list',
returnType: `Promise<{cursor?: string, records: ({uri: string, value: ${typeModule}.Record})[]}>`,
});
method.addParameter({
name: 'params',
type: `OmitKey<${(0, util_1.toTitleCase)(ATP_METHODS.list)}.QueryParams, "collection">`,
});
method.setBodyText([
`const res = await this._client.call('${ATP_METHODS.list}', { collection: '${nsid}', ...params })`,
`return res.data`,
].join('\n'));
}
{
//= get()
const method = cls.addMethod({
isAsync: true,
name: 'get',
returnType: `Promise<{uri: string, cid: string, value: ${typeModule}.Record}>`,
});
method.addParameter({
name: 'params',
type: `OmitKey<${(0, util_1.toTitleCase)(ATP_METHODS.get)}.QueryParams, "collection">`,
});
method.setBodyText([
`const res = await this._client.call('${ATP_METHODS.get}', { collection: '${nsid}', ...params })`,
`return res.data`,
].join('\n'));
}
{
//= create()
const method = cls.addMethod({
isAsync: true,
name: 'create',
returnType: 'Promise<{uri: string, cid: string}>',
});
method.addParameter({
name: 'params',
type: `OmitKey<${(0, util_1.toTitleCase)(ATP_METHODS.create)}.InputSchema, "collection" | "record">`,
});
method.addParameter({
name: 'record',
type: `Un$Typed<${typeModule}.Record>`,
});
method.addParameter({
name: 'headers?',
type: `Record<string, string>`,
});
const maybeRkeyPart = lexRecord.key?.startsWith('literal:')
? `rkey: '${lexRecord.key.replace('literal:', '')}', `
: '';
method.setBodyText([
`const collection = '${nsid}'`,
`const res = await this._client.call('${ATP_METHODS.create}', undefined, { collection, ${maybeRkeyPart}...params, record: { ...record, $type: collection } }, { encoding: 'application/json', headers })`,
`return res.data`,
].join('\n'));
}
{
//= put()
const method = cls.addMethod({
isAsync: true,
name: 'put',
returnType: 'Promise<{uri: string, cid: string}>',
});
method.addParameter({
name: 'params',
type: `OmitKey<${(0, util_1.toTitleCase)(ATP_METHODS.put)}.InputSchema, "collection" | "record">`,
});
method.addParameter({
name: 'record',
type: `Un$Typed<${typeModule}.Record>`,
});
method.addParameter({
name: 'headers?',
type: `Record<string, string>`,
});
method.setBodyText([
`const collection = '${nsid}'`,
`const res = await this._client.call('${ATP_METHODS.put}', undefined, { collection, ...params, record: { ...record, $type: collection } }, { encoding: 'application/json', headers })`,
`return res.data`,
].join('\n'));
}
{
//= delete()
const method = cls.addMethod({
isAsync: true,
name: 'delete',
returnType: 'Promise<void>',
});
method.addParameter({
name: 'params',
type: `OmitKey<${(0, util_1.toTitleCase)(ATP_METHODS.delete)}.InputSchema, "collection">`,
});
method.addParameter({
name: 'headers?',
type: `Record<string, string>`,
});
method.setBodyText([
`await this._client.call('${ATP_METHODS.delete}', undefined, { collection: '${nsid}', ...params }, { headers })`,
].join('\n'));
}
}
const lexiconTs = (project, lexicons, lexiconDoc) => (0, common_1.gen)(project, `/types/${lexiconDoc.id.split('.').join('/')}.ts`, async (file) => {
const main = lexiconDoc.defs.main;
if (main?.type === 'query' ||
main?.type === 'subscription' ||
main?.type === 'procedure') {
//= import {HeadersMap, XRPCError} from '@atproto/xrpc'
const xrpcImport = file.addImportDeclaration({
moduleSpecifier: '@atproto/xrpc',
});
xrpcImport.addNamedImports([
{ name: 'HeadersMap' },
{ name: 'XRPCError' },
]);
}
(0, lex_gen_1.genCommonImports)(file, lexiconDoc.id);
const imports = new Set();
for (const defId in lexiconDoc.defs) {
const def = lexiconDoc.defs[defId];
const lexUri = `${lexiconDoc.id}#${defId}`;
if (defId === 'main') {
if (def.type === 'query' || def.type === 'procedure') {
(0, lex_gen_1.genXrpcParams)(file, lexicons, lexUri, false);
(0, lex_gen_1.genXrpcInput)(file, imports, lexicons, lexUri, false);
(0, lex_gen_1.genXrpcOutput)(file, imports, lexicons, lexUri);
genClientXrpcCommon(file, lexicons, lexUri);
}
else if (def.type === 'subscription') {
continue;
}
else if (def.type === 'record') {
(0, lex_gen_1.genRecord)(file, imports, lexicons, lexUri);
}
else {
(0, lex_gen_1.genUserType)(file, imports, lexicons, lexUri);
}
}
else {
(0, lex_gen_1.genUserType)(file, imports, lexicons, lexUri);
}
}
(0, lex_gen_1.genImports)(file, imports, lexiconDoc.id);
});
function genClientXrpcCommon(file, lexicons, lexUri) {
const def = lexicons.getDefOrThrow(lexUri, ['query', 'procedure']);
//= export interface CallOptions {...}
const opts = file.addInterface({
name: 'CallOptions',
isExported: true,
});
opts.addProperty({ name: 'signal?', type: 'AbortSignal' });
opts.addProperty({ name: 'headers?', type: 'HeadersMap' });
if (def.type === 'procedure') {
opts.addProperty({ name: 'qp?', type: 'QueryParams' });
}
if (def.type === 'procedure' && def.input) {
let encodingType = 'string';
if (def.input.encoding !== '*/*') {
encodingType = def.input.encoding
.split(',')
.map((v) => `'${v.trim()}'`)
.join(' | ');
}
opts.addProperty({
name: 'encoding?',
type: encodingType,
});
}
// export interface Response {...}
const res = file.addInterface({
name: 'Response',
isExported: true,
});
res.addProperty({ name: 'success', type: 'boolean' });
res.addProperty({ name: 'headers', type: 'HeadersMap' });
if (def.output?.schema) {
if (def.output.encoding?.includes(',')) {
res.addProperty({ name: 'data', type: 'OutputSchema | Uint8Array' });
}
else {
res.addProperty({ name: 'data', type: 'OutputSchema' });
}
}
else if (def.output?.encoding) {
res.addProperty({ name: 'data', type: 'Uint8Array' });
}
// export class {errcode}Error {...}
const customErrors = [];
for (const error of def.errors || []) {
let name = (0, util_1.toTitleCase)(error.name);
if (!name.endsWith('Error'))
name += 'Error';
const errCls = file.addClass({
name,
extends: 'XRPCError',
isExported: true,
});
errCls.addConstructor({
parameters: [{ name: 'src', type: 'XRPCError' }],
statements: [
'super(src.status, src.error, src.message, src.headers, { cause: src })',
],
});
customErrors.push({ name: error.name, cls: name });
}
// export function toKnownErr(err: any) {...}
file.addFunction({
name: 'toKnownErr',
isExported: true,
parameters: [{ name: 'e', type: 'any' }],
statements: customErrors.length
? [
'if (e instanceof XRPCError) {',
...customErrors.map((err) => `if (e.error === '${err.name}') return new ${err.cls}(e)`),
'}',
'return e',
]
: ['return e'],
});
}
//# sourceMappingURL=client.js.map