Something went wrong. Try again.
Highly ambitious ATProtocol AppView service and sdks forked from slices.network/slices
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396// @ts-ignore: ts-morph npm importimport { Project } from "ts-morph";
export interface LexiconProperty { type: string; description?: string; format?: string; knownValues?: string[]; items?: LexiconProperty; ref?: string; refs?: string[]; closed?: boolean; [key: string]: unknown;}
export interface LexiconRecord { type: string; properties?: Record<string, LexiconProperty>; required?: string[];}
export interface LexiconParameters { type: "params"; properties?: Record<string, LexiconProperty>; required?: string[];}
export interface LexiconIO { encoding: string; schema?: LexiconProperty;}
export interface LexiconDefinition { type: string; description?: string; // Record type fields record?: LexiconRecord; key?: string; // Query/Procedure fields parameters?: LexiconParameters; input?: LexiconIO; output?: LexiconIO; // Generic schema fields properties?: Record<string, LexiconProperty>; required?: string[]; refs?: string[]; closed?: boolean; items?: LexiconProperty; knownValues?: string[]; ref?: string; [key: string]: unknown;}
export interface Lexicon { id: string; definitions?: Record<string, LexiconDefinition>;}
export interface GenerateOptions { sliceUri: string;}
// Normalize lexicons to use consistent property namesexport function normalizeLexicons(lexicons: unknown[]): Lexicon[] { return lexicons.map((lex: unknown) => { const lexObj = lex as Record<string, unknown>; return { ...lexObj, // TODO: Fix upstream to use 'id' and 'defs' consistently id: (lexObj.id as string) || (lexObj.nsid as string), // Normalize nsid to id definitions: (lexObj.defs as Record<string, LexiconDefinition>) || (lexObj.definitions as Record<string, LexiconDefinition>), // Normalize defs to definitions }; });}
// Convert NSID to PascalCase (for record types, no "Record" suffix)export function nsidToPascalCase(nsid: string): string { return nsid .split(".") .map((part) => part .split(/[-_]/) .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join("") ) .join("");}
// Convert NSID to PascalCase for namespaces (without "Record" suffix)export function nsidToNamespace(nsid: string): string { return nsid .split(".") .map((part) => part .split(/[-_]/) .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join("") ) .join("");}
// Convert definition name to PascalCaseexport function defNameToPascalCase(defName: string): string { return defName .split(/[-_]/) .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join("");}
// Capitalize first letterexport function capitalizeFirst(str: string): string { return str.charAt(0).toUpperCase() + str.slice(1);}
// Sanitize a string to be a valid JavaScript identifierexport function sanitizeIdentifier(str: string): string { // Replace hyphens and other non-alphanumeric characters with camelCase return str .split(/[-_]/) .map((word, index) => index === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1) ) .join("");}
// Check if property is requiredexport function isPropertyRequired( recordObj: LexiconRecord, propName: string): boolean { return Boolean(recordObj.required && recordObj.required.includes(propName));}
// Helper function to check if a field type is sortableexport function isFieldSortable(propDef: LexiconProperty): boolean { // Check for direct types if (propDef.type) { const sortableTypes = ["string", "integer", "number", "datetime"]; if (sortableTypes.includes(propDef.type)) { return true; } }
// Check for format-based types (datetime strings, etc.) if (propDef.format) { const sortableFormats = ["datetime", "at-identifier", "at-uri"]; if (sortableFormats.includes(propDef.format)) { return true; } }
// Arrays, objects, blobs, and complex types are not sortable return false;}
export function createProject(): Project { return new Project({ useInMemoryFileSystem: true });}
export function generateUsageExample( lexicons: Lexicon[], sliceUri: string): string { // Find the first non-network.slices lexicon that has a record type const nonSlicesLexicon = lexicons.find( (lex) => lex.id && !lex.id.startsWith("network.slices.") && lex.definitions && Object.values(lex.definitions).some((def) => def.type === "record") );
if (nonSlicesLexicon) { // Use the first non-slices lexicon const parts = nonSlicesLexicon.id.split("."); // Sanitize parts for JavaScript property access (skip first part "network") const accessPath = parts.map((part, index) => index === 0 ? part : sanitizeIdentifier(part) ).join(".");
return `/** * @example Usage * \`\`\`ts * import { AtProtoClient } from "./generated_client.ts"; * * const client = new AtProtoClient( * 'https://api.slices.network', * '${sliceUri}' * ); * * // Get records from the ${nonSlicesLexicon.id} collection * const records = await client.${accessPath}.getRecords(); * * // Get a specific record * const record = await client.${accessPath}.getRecord({ * uri: 'at://did:plc:example/${nonSlicesLexicon.id}/3abc123' * }); * * // Get records with filtering and search * const filteredRecords = await client.${accessPath}.getRecords({ * where: { * text: { contains: "example search term" } * } * }); * * // Use slice-level methods for cross-collection queries with type safety * const sliceRecords = await client.network.slices.slice.getSliceRecords<${nsidToPascalCase( nonSlicesLexicon.id )}>({ * where: { * collection: { eq: '${nonSlicesLexicon.id}' } * } * }); * * // Search across multiple collections using union types * const multiCollectionRecords = await client.network.slices.slice.getSliceRecords<${nsidToPascalCase( nonSlicesLexicon.id )} | AppBskyActorProfile>({ * where: { * collection: { in: ['${nonSlicesLexicon.id}', 'app.bsky.actor.profile'] }, * text: { contains: 'example search term' }, * did: { in: ['did:plc:user1', 'did:plc:user2'] } * }, * limit: 20 * }); * * // Serve the records as JSON * Deno.serve(async () => new Response(JSON.stringify(records.records.map(r => r.value)))); * \`\`\` */`; } else { // Fallback: find any lexicon with a record type (including network.slices) const anyRecordLexicon = lexicons.find( (lex) => lex.definitions && Object.values(lex.definitions).some((def) => def.type === "record") );
if (anyRecordLexicon) { const parts = anyRecordLexicon.id.split("."); // Sanitize parts for JavaScript property access (skip first part "network") const accessPath = parts.map((part, index) => index === 0 ? part : sanitizeIdentifier(part) ).join(".");
return `/** * @example Usage * \`\`\`ts * import { AtProtoClient } from "./generated_client.ts"; * * const client = new AtProtoClient( * 'https://api.slices.network', * '${sliceUri}' * ); * * // Get records from the ${anyRecordLexicon.id} collection * const records = await client.${accessPath}.getRecords(); * * // Get a specific record * const record = await client.${accessPath}.getRecord({ * uri: 'at://did:plc:example/${anyRecordLexicon.id}/3abc123' * }); * * // Get records with search and filtering * const filteredRecords = await client.${accessPath}.getRecords({ * where: { * text: { contains: "example search term" } * } * }); * * // Cross-collection operations using base client with type safety * const multiCollectionResults = await client.getSliceRecords<${nsidToPascalCase( anyRecordLexicon.id )}>({ * where: { * collection: { eq: '${anyRecordLexicon.id}' } * }, * limit: 50 * }); * * // Serve the records as JSON * Deno.serve(async () => new Response(JSON.stringify(records.records.map(r => r.value)))); * \`\`\` */`; }
// Final fallback if no record types exist return `/** * @example Usage * \`\`\`ts * import { AtProtoClient } from "./generated_client.ts"; * * const client = new AtProtoClient( * 'https://api.slices.network', * '${sliceUri}' * ); * * // Client is ready to use with available collections * // Use client methods based on your lexicon definitions * \`\`\` */`; }}
export function generateHeaderComment( lexicons: Lexicon[], usageExample: string): string { return `// Generated TypeScript client for AT Protocol records// Generated at: ${new Date().toISOString().slice(0, 19).replace("T", " ")} UTC// Lexicons: ${lexicons.length}
${usageExample}
import { SlicesClient, type RecordResponse, type GetRecordsResponse, type CountRecordsResponse, type GetRecordParams, type WhereCondition, type IndexedRecordFields, type SortField, type BlobRef, type AuthProvider } from "@slices/client";import type { OAuthClient } from "@slices/oauth";
`;}
// Format the code using deno fmt via temp fileexport async function formatCode(code: string): Promise<string> { try { const tempFile = await Deno.makeTempFile({ suffix: ".ts" });
// Write unformatted code to temp file await Deno.writeTextFile(tempFile, code);
// Format the temp file const process = new Deno.Command("deno", { args: ["fmt", tempFile], stdout: "piped", stderr: "piped", });
const output = await process.output();
if (output.success) { // Read the formatted code back const formattedCode = await Deno.readTextFile(tempFile); await Deno.remove(tempFile); return formattedCode; } else { const error = new TextDecoder().decode(output.stderr); console.warn("deno fmt failed, using unformatted code:", error); await Deno.remove(tempFile); return code; } } catch (error) { console.warn("deno fmt not available, using unformatted code:", error); return code; }}
// Convenience function that combines everythingexport async function generateTypeScript( lexicons: unknown[], options: GenerateOptions): Promise<string> { const { generateInterfaces } = await import("./interfaces.ts"); const { generateClient } = await import("./client.ts");
const normalizedLexicons = normalizeLexicons(lexicons); const project = createProject(); const sourceFile = project.createSourceFile("generated-client.ts", "");
const usageExample = generateUsageExample( normalizedLexicons, options.sliceUri ); const headerComment = generateHeaderComment( normalizedLexicons, usageExample );
// Add header comment and imports to the source file first sourceFile.insertText(0, headerComment);
// Generate interfaces and client generateInterfaces( sourceFile, normalizedLexicons ); generateClient( sourceFile, normalizedLexicons );
// Get the generated code const generatedCode = sourceFile.getFullText();
return await formatCode(generatedCode);}