Highly ambitious ATProtocol AppView service and sdks
at fix-postgres 426 lines 15 kB view raw
1import type { SourceFile } from "ts-morph"; 2import { Scope } from "ts-morph"; 3import type { Lexicon } from "./mod.ts"; 4import { nsidToPascalCase, capitalizeFirst, sanitizeIdentifier } from "./mod.ts"; 5 6interface NestedStructure { 7 [key: string]: NestedStructure | string | QueryProcedureInfo[] | undefined; 8 _recordType?: string; 9 _collectionPath?: string; 10 _queryProcedures?: QueryProcedureInfo[]; 11} 12 13interface QueryProcedureInfo { 14 nsid: string; 15 type: "query" | "procedure"; 16 methodName: string; 17 parametersType?: string; 18 inputType?: string; 19 outputType?: string; 20} 21 22interface PropertyInfo { 23 name: string; 24 type: string; 25} 26 27interface MethodInfo { 28 name: string; 29 parameters: Array<{ name: string; type: string; hasQuestionToken?: boolean }>; 30 returnType: string; 31} 32 33export function generateClient( 34 sourceFile: SourceFile, 35 lexicons: Lexicon[] 36): void { 37 // Create nested structure from lexicons 38 const nestedStructure: NestedStructure = {}; 39 40 for (const lexicon of lexicons) { 41 if (lexicon.definitions && typeof lexicon.definitions === "object") { 42 // Check if this lexicon has any records, queries, or procedures 43 const hasRecordsOrEndpoints = Object.values(lexicon.definitions).some( 44 (defValue) => 45 (defValue.type === "record" && defValue.record) || 46 defValue.type === "query" || 47 defValue.type === "procedure" 48 ); 49 50 // Only build nested structure for lexicons that have records 51 if (hasRecordsOrEndpoints) { 52 for (const [_, defValue] of Object.entries(lexicon.definitions)) { 53 if (defValue.type === "record" && defValue.record) { 54 const parts = lexicon.id.split("."); 55 let current = nestedStructure; 56 57 // Build nested structure 58 for (const part of parts) { 59 if (!current[part]) { 60 current[part] = {}; 61 } 62 current = current[part] as NestedStructure; 63 } 64 65 // Add the record interface name and store collection path 66 current._recordType = nsidToPascalCase(lexicon.id); 67 current._collectionPath = lexicon.id; 68 } 69 } 70 } 71 } 72 } 73 74 // Add query/procedure methods to the appropriate parent clients 75 function addQueryProcedureMethods( 76 obj: NestedStructure, 77 lexicons: Lexicon[] 78 ): void { 79 // Group query/procedure endpoints by their parent collection path 80 const endpointsByParent = new Map<string, QueryProcedureInfo[]>(); 81 82 for (const lexicon of lexicons) { 83 if (lexicon.definitions && typeof lexicon.definitions === "object") { 84 for (const [_, defValue] of Object.entries(lexicon.definitions)) { 85 if (defValue.type === "query" || defValue.type === "procedure") { 86 const parts = lexicon.id.split("."); 87 const methodName = parts[parts.length - 1]; 88 89 // Find the parent collection by removing the method name 90 // e.g., "network.slices.slice.getJobStatus" -> "network.slices.slice" 91 const parentPath = parts.slice(0, -1).join("."); 92 93 const queryProcedureInfo: QueryProcedureInfo = { 94 nsid: lexicon.id, 95 type: defValue.type as "query" | "procedure", 96 methodName, 97 parametersType: 98 defValue.parameters?.properties && 99 Object.keys(defValue.parameters.properties).length > 0 100 ? `${nsidToPascalCase(lexicon.id)}Params` 101 : undefined, 102 inputType: defValue.input 103 ? `${nsidToPascalCase(lexicon.id)}Input` 104 : undefined, 105 outputType: defValue.output 106 ? `${nsidToPascalCase(lexicon.id)}Output` 107 : undefined, 108 }; 109 110 if (!endpointsByParent.has(parentPath)) { 111 endpointsByParent.set(parentPath, []); 112 } 113 endpointsByParent.get(parentPath)!.push(queryProcedureInfo); 114 } 115 } 116 } 117 } 118 119 // Add endpoints to their respective parent clients 120 function addEndpointsToNode( 121 current: NestedStructure, 122 path: string[], 123 fullPath: string 124 ): void { 125 if (path.length === 0) { 126 // We've reached the target node, add the endpoints if this has a record type 127 const endpoints = endpointsByParent.get(fullPath); 128 if (endpoints && current._recordType) { 129 if (!current._queryProcedures) { 130 current._queryProcedures = []; 131 } 132 current._queryProcedures.push(...endpoints); 133 } 134 return; 135 } 136 137 const [head, ...tail] = path; 138 if (current[head]) { 139 addEndpointsToNode(current[head] as NestedStructure, tail, fullPath); 140 } 141 } 142 143 // Add endpoints to their parent nodes 144 for (const parentPath of endpointsByParent.keys()) { 145 const pathParts = parentPath.split("."); 146 addEndpointsToNode(obj, pathParts, parentPath); 147 } 148 } 149 150 // Add query/procedure methods before generating classes 151 addQueryProcedureMethods(nestedStructure, lexicons); 152 153 // Generate nested class structure 154 function generateNestedClass( 155 obj: NestedStructure, 156 className = "CollectionNode", 157 currentPath: string[] = [] 158 ): void { 159 const properties: PropertyInfo[] = []; 160 const methods: MethodInfo[] = []; 161 162 let collectionPath = ""; 163 164 for (const [key, value] of Object.entries(obj)) { 165 if (key === "_recordType") { 166 // Add collection operations for this record type 167 const recordName = value as string; 168 // Check if we have sortable fields for this record 169 const hasSortFields = sourceFile.getTypeAlias( 170 `${recordName}SortFields` 171 ); 172 const sortFieldsType = hasSortFields 173 ? `${recordName}SortFields` 174 : "IndexedRecordFields"; 175 const whereFieldsType = hasSortFields 176 ? `${recordName}SortFields | IndexedRecordFields` 177 : "IndexedRecordFields"; 178 179 methods.push({ 180 name: "getRecords", 181 parameters: [ 182 { 183 name: "params", 184 type: `{ limit?: number; cursor?: string; where?: { [K in ${whereFieldsType}]?: WhereCondition }; orWhere?: { [K in ${whereFieldsType}]?: WhereCondition }; sortBy?: SortField<${sortFieldsType}>[]; }`, 185 hasQuestionToken: true, 186 }, 187 ], 188 returnType: `Promise<GetRecordsResponse<${value}>>`, 189 }); 190 methods.push({ 191 name: "getRecord", 192 parameters: [{ name: "params", type: "GetRecordParams" }], 193 returnType: `Promise<RecordResponse<${value}>>`, 194 }); 195 methods.push({ 196 name: "countRecords", 197 parameters: [ 198 { 199 name: "params", 200 type: `{ limit?: number; cursor?: string; where?: { [K in ${whereFieldsType}]?: WhereCondition }; orWhere?: { [K in ${whereFieldsType}]?: WhereCondition }; sortBy?: SortField<${sortFieldsType}>[]; }`, 201 hasQuestionToken: true, 202 }, 203 ], 204 returnType: "Promise<CountRecordsResponse>", 205 }); 206 // Add create, update, delete methods 207 methods.push({ 208 name: "createRecord", 209 parameters: [ 210 { name: "record", type: value as string }, 211 { name: "useSelfRkey", type: "boolean", hasQuestionToken: true }, 212 ], 213 returnType: `Promise<{ uri: string; cid: string }>`, 214 }); 215 methods.push({ 216 name: "updateRecord", 217 parameters: [ 218 { name: "rkey", type: "string" }, 219 { name: "record", type: value as string }, 220 ], 221 returnType: `Promise<{ uri: string; cid: string }>`, 222 }); 223 methods.push({ 224 name: "deleteRecord", 225 parameters: [{ name: "rkey", type: "string" }], 226 returnType: `Promise<void>`, 227 }); 228 } else if (key === "_collectionPath") { 229 collectionPath = value as string; 230 } else if (key === "_queryProcedures") { 231 // Add query and procedure methods 232 const queryProcedures = value as QueryProcedureInfo[]; 233 for (const qp of queryProcedures) { 234 if (qp.type === "query") { 235 // Generate query method (GET) 236 const parameters = []; 237 if (qp.parametersType) { 238 parameters.push({ 239 name: "params", 240 type: qp.parametersType, 241 hasQuestionToken: true, 242 }); 243 } 244 methods.push({ 245 name: qp.methodName, 246 parameters, 247 returnType: `Promise<${qp.outputType || "void"}>`, 248 }); 249 } else if (qp.type === "procedure") { 250 // Generate procedure method (POST) 251 const parameters = []; 252 if (qp.inputType) { 253 parameters.push({ 254 name: "input", 255 type: qp.inputType, 256 }); 257 } else if (qp.parametersType) { 258 parameters.push({ 259 name: "params", 260 type: qp.parametersType, 261 }); 262 } 263 methods.push({ 264 name: qp.methodName, 265 parameters, 266 returnType: `Promise<${qp.outputType || "void"}>`, 267 }); 268 } 269 } 270 } else if (typeof value === "object" && Object.keys(value).length > 0) { 271 // Add nested property with PascalCase class name 272 // Sanitize the key for both class name and property name 273 const sanitizedKey = sanitizeIdentifier(key); 274 const nestedClassName = `${capitalizeFirst(sanitizedKey)}${className}`; 275 generateNestedClass(value as NestedStructure, nestedClassName, [ 276 ...currentPath, 277 key, 278 ]); 279 properties.push({ 280 name: sanitizedKey, 281 type: nestedClassName, 282 }); 283 } 284 } 285 286 if (properties.length > 0 || methods.length > 0) { 287 // Use proper naming for the main client 288 const finalClassName = 289 className === "Client" ? "AtProtoClient" : className; 290 291 const classDeclaration = sourceFile.addClass({ 292 name: finalClassName, 293 isExported: className === "Client", 294 extends: className === "Client" ? "SlicesClient" : undefined, 295 properties: [ 296 ...properties.map((p) => ({ 297 name: p.name, 298 type: p.type, 299 isReadonly: true, 300 })), 301 // Add OAuth client to the main AtProtoClient 302 ...(className === "Client" 303 ? [ 304 { 305 name: "oauth", 306 type: "OAuthClient | AuthProvider", 307 isReadonly: true, 308 hasQuestionToken: true, 309 }, 310 ] 311 : [ 312 // Nested classes need a reference to the client 313 { 314 name: "client", 315 type: "SlicesClient", 316 scope: Scope.Private, 317 isReadonly: true, 318 }, 319 ]), 320 ], 321 }); 322 323 // Add constructor 324 const ctor = classDeclaration.addConstructor({ 325 parameters: 326 className === "Client" 327 ? [ 328 { name: "baseUrl", type: "string" }, 329 { name: "sliceUri", type: "string" }, 330 { 331 name: "oauthClient", 332 type: "OAuthClient | AuthProvider", 333 hasQuestionToken: true, 334 }, 335 ] 336 : [{ name: "client", type: "SlicesClient" }], 337 }); 338 339 if (className === "Client") { 340 ctor.addStatements([ 341 "super(baseUrl, sliceUri, oauthClient);", 342 ...properties.map((p) => `this.${p.name} = new ${p.type}(this);`), 343 "this.oauth = oauthClient;", 344 ]); 345 } else { 346 // Nested classes store reference to parent client 347 ctor.addStatements([ 348 "this.client = client;", 349 ...properties.map((p) => `this.${p.name} = new ${p.type}(client);`), 350 ]); 351 } 352 353 // Add methods with implementations 354 for (const method of methods) { 355 const methodDecl = classDeclaration.addMethod({ 356 name: method.name, 357 parameters: method.parameters, 358 returnType: method.returnType, 359 isAsync: true, 360 }); 361 362 // Add basic implementation using shared client methods 363 // Use this.client for nested classes, this for main client 364 const clientRef = className === "Client" ? "this" : "this.client"; 365 366 if (method.name === "getRecords") { 367 methodDecl.addStatements([ 368 `return await ${clientRef}.getRecords('${collectionPath}', params);`, 369 ]); 370 } else if (method.name === "getRecord") { 371 methodDecl.addStatements([ 372 `return await ${clientRef}.getRecord('${collectionPath}', params);`, 373 ]); 374 } else if (method.name === "createRecord") { 375 methodDecl.addStatements([ 376 `return await ${clientRef}.createRecord('${collectionPath}', record, useSelfRkey);`, 377 ]); 378 } else if (method.name === "updateRecord") { 379 methodDecl.addStatements([ 380 `return await ${clientRef}.updateRecord('${collectionPath}', rkey, record);`, 381 ]); 382 } else if (method.name === "deleteRecord") { 383 methodDecl.addStatements([ 384 `return await ${clientRef}.deleteRecord('${collectionPath}', rkey);`, 385 ]); 386 } else if (method.name === "countRecords") { 387 methodDecl.addStatements([ 388 `return await ${clientRef}.countRecords('${collectionPath}', params);`, 389 ]); 390 } else { 391 // Handle query and procedure methods 392 const queryProcedures = obj._queryProcedures || []; 393 const matchingQP = queryProcedures.find( 394 (qp) => qp.methodName === method.name 395 ); 396 397 if (matchingQP) { 398 if (matchingQP.type === "query") { 399 // Query methods use GET with query parameters 400 const paramArg = method.parameters.length > 0 ? "params" : "{}"; 401 methodDecl.addStatements([ 402 `return await ${clientRef}.makeRequest<${ 403 matchingQP.outputType || "void" 404 }>('${matchingQP.nsid}', 'GET', ${paramArg});`, 405 ]); 406 } else if (matchingQP.type === "procedure") { 407 // Procedure methods use POST with body 408 const paramArg = 409 method.parameters.length > 0 ? method.parameters[0].name : "{}"; 410 methodDecl.addStatements([ 411 `return await ${clientRef}.makeRequest<${ 412 matchingQP.outputType || "void" 413 }>('${matchingQP.nsid}', 'POST', ${paramArg});`, 414 ]); 415 } 416 } 417 } 418 } 419 } 420 } 421 422 // Generate the main client class 423 if (Object.keys(nestedStructure).length > 0) { 424 generateNestedClass(nestedStructure, "Client"); 425 } 426}