Highly ambitious ATProtocol AppView service and sdks

sanitize nsids properly like cosmic-flux-123 in codegen interfaces

+79 -9
+5 -3
packages/codegen/src/client.ts
··· 1 import type { SourceFile } from "ts-morph"; 2 import { Scope } from "ts-morph"; 3 import type { Lexicon } from "./mod.ts"; 4 - import { nsidToPascalCase, capitalizeFirst } from "./mod.ts"; 5 6 interface NestedStructure { 7 [key: string]: NestedStructure | string | QueryProcedureInfo[] | undefined; ··· 269 } 270 } else if (typeof value === "object" && Object.keys(value).length > 0) { 271 // Add nested property with PascalCase class name 272 - const nestedClassName = `${capitalizeFirst(key)}${className}`; 273 generateNestedClass(value as NestedStructure, nestedClassName, [ 274 ...currentPath, 275 key, 276 ]); 277 properties.push({ 278 - name: key, 279 type: nestedClassName, 280 }); 281 }
··· 1 import type { SourceFile } from "ts-morph"; 2 import { Scope } from "ts-morph"; 3 import type { Lexicon } from "./mod.ts"; 4 + import { nsidToPascalCase, capitalizeFirst, sanitizeIdentifier } from "./mod.ts"; 5 6 interface NestedStructure { 7 [key: string]: NestedStructure | string | QueryProcedureInfo[] | undefined; ··· 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 }
+26 -6
packages/codegen/src/mod.ts
··· 114 return str.charAt(0).toUpperCase() + str.slice(1); 115 } 116 117 // Check if property is required 118 export function isPropertyRequired( 119 recordObj: LexiconRecord, ··· 164 if (nonSlicesLexicon) { 165 // Use the first non-slices lexicon 166 const parts = nonSlicesLexicon.id.split("."); 167 168 return `/** 169 * @example Usage ··· 176 * ); 177 * 178 * // Get records from the ${nonSlicesLexicon.id} collection 179 - * const records = await client.${parts.join(".")}.getRecords(); 180 * 181 * // Get a specific record 182 - * const record = await client.${parts.join(".")}.getRecord({ 183 * uri: 'at://did:plc:example/${nonSlicesLexicon.id}/3abc123' 184 * }); 185 * 186 * // Get records with filtering and search 187 - * const filteredRecords = await client.${parts.join(".")}.getRecords({ 188 * where: { 189 * text: { contains: "example search term" } 190 * } ··· 225 226 if (anyRecordLexicon) { 227 const parts = anyRecordLexicon.id.split("."); 228 return `/** 229 * @example Usage 230 * \`\`\`ts ··· 236 * ); 237 * 238 * // Get records from the ${anyRecordLexicon.id} collection 239 - * const records = await client.${parts.join(".")}.getRecords(); 240 * 241 * // Get a specific record 242 - * const record = await client.${parts.join(".")}.getRecord({ 243 * uri: 'at://did:plc:example/${anyRecordLexicon.id}/3abc123' 244 * }); 245 * 246 * // Get records with search and filtering 247 - * const filteredRecords = await client.${parts.join(".")}.getRecords({ 248 * where: { 249 * text: { contains: "example search term" } 250 * }
··· 114 return str.charAt(0).toUpperCase() + str.slice(1); 115 } 116 117 + // Sanitize a string to be a valid JavaScript identifier 118 + export function sanitizeIdentifier(str: string): string { 119 + // Replace hyphens and other non-alphanumeric characters with camelCase 120 + return str 121 + .split(/[-_]/) 122 + .map((word, index) => 123 + index === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1) 124 + ) 125 + .join(""); 126 + } 127 + 128 // Check if property is required 129 export function isPropertyRequired( 130 recordObj: LexiconRecord, ··· 175 if (nonSlicesLexicon) { 176 // Use the first non-slices lexicon 177 const parts = nonSlicesLexicon.id.split("."); 178 + // Sanitize parts for JavaScript property access (skip first part "network") 179 + const accessPath = parts.map((part, index) => 180 + index === 0 ? part : sanitizeIdentifier(part) 181 + ).join("."); 182 183 return `/** 184 * @example Usage ··· 191 * ); 192 * 193 * // Get records from the ${nonSlicesLexicon.id} collection 194 + * const records = await client.${accessPath}.getRecords(); 195 * 196 * // Get a specific record 197 + * const record = await client.${accessPath}.getRecord({ 198 * uri: 'at://did:plc:example/${nonSlicesLexicon.id}/3abc123' 199 * }); 200 * 201 * // Get records with filtering and search 202 + * const filteredRecords = await client.${accessPath}.getRecords({ 203 * where: { 204 * text: { contains: "example search term" } 205 * } ··· 240 241 if (anyRecordLexicon) { 242 const parts = anyRecordLexicon.id.split("."); 243 + // Sanitize parts for JavaScript property access (skip first part "network") 244 + const accessPath = parts.map((part, index) => 245 + index === 0 ? part : sanitizeIdentifier(part) 246 + ).join("."); 247 + 248 return `/** 249 * @example Usage 250 * \`\`\`ts ··· 256 * ); 257 * 258 * // Get records from the ${anyRecordLexicon.id} collection 259 + * const records = await client.${accessPath}.getRecords(); 260 * 261 * // Get a specific record 262 + * const record = await client.${accessPath}.getRecord({ 263 * uri: 'at://did:plc:example/${anyRecordLexicon.id}/3abc123' 264 * }); 265 * 266 * // Get records with search and filtering 267 + * const filteredRecords = await client.${accessPath}.getRecords({ 268 * where: { 269 * text: { contains: "example search term" } 270 * }
+38
packages/codegen/tests/client_test.ts
··· 150 assertStringIncludes(result, "async createRecord("); 151 }); 152 153 Deno.test("generateClient - handles mixed lexicon types", () => { 154 const project = createTestProject(); 155 const sourceFile = project.createSourceFile("test.ts", "");
··· 150 assertStringIncludes(result, "async createRecord("); 151 }); 152 153 + Deno.test("generateClient - sanitizes hyphenated NSIDs", () => { 154 + const project = createTestProject(); 155 + const sourceFile = project.createSourceFile("test.ts", ""); 156 + 157 + const lexicons: Lexicon[] = [ 158 + { 159 + id: "network.slices.cyber-meteor-1637.actor.profile", 160 + definitions: { 161 + main: { 162 + type: "record", 163 + record: { 164 + type: "record", 165 + properties: { 166 + displayName: { type: "string" }, 167 + description: { type: "string" }, 168 + }, 169 + }, 170 + }, 171 + }, 172 + }, 173 + ]; 174 + 175 + generateClient(sourceFile, lexicons); 176 + const result = sourceFile.getFullText(); 177 + 178 + // Should sanitize hyphenated names to camelCase 179 + assertStringIncludes(result, "readonly cyberMeteor1637: CyberMeteor1637SlicesNetworkClient;"); 180 + assertStringIncludes(result, "class CyberMeteor1637SlicesNetworkClient"); 181 + assertStringIncludes(result, "class ActorCyberMeteor1637SlicesNetworkClient"); 182 + assertStringIncludes(result, "class ProfileActorCyberMeteor1637SlicesNetworkClient"); 183 + 184 + // Constructor should use sanitized property names 185 + assertStringIncludes(result, "this.cyberMeteor1637 = new CyberMeteor1637SlicesNetworkClient("); 186 + 187 + // But should preserve original NSID in API calls 188 + assertStringIncludes(result, "'network.slices.cyber-meteor-1637.actor.profile'"); 189 + }); 190 + 191 Deno.test("generateClient - handles mixed lexicon types", () => { 192 const project = createTestProject(); 193 const sourceFile = project.createSourceFile("test.ts", "");
+10
packages/codegen/tests/utils_test.ts
··· 5 nsidToNamespace, 6 defNameToPascalCase, 7 capitalizeFirst, 8 isPropertyRequired, 9 isFieldSortable, 10 createProject, ··· 50 assertEquals(capitalizeFirst("hello"), "Hello"); 51 assertEquals(capitalizeFirst("world"), "World"); 52 assertEquals(capitalizeFirst(""), ""); 53 }); 54 55 Deno.test("isPropertyRequired - checks if property is required", () => {
··· 5 nsidToNamespace, 6 defNameToPascalCase, 7 capitalizeFirst, 8 + sanitizeIdentifier, 9 isPropertyRequired, 10 isFieldSortable, 11 createProject, ··· 51 assertEquals(capitalizeFirst("hello"), "Hello"); 52 assertEquals(capitalizeFirst("world"), "World"); 53 assertEquals(capitalizeFirst(""), ""); 54 + }); 55 + 56 + Deno.test("sanitizeIdentifier - converts hyphens to camelCase", () => { 57 + assertEquals(sanitizeIdentifier("cyber-meteor-1637"), "cyberMeteor1637"); 58 + assertEquals(sanitizeIdentifier("test-name"), "testName"); 59 + assertEquals(sanitizeIdentifier("already-camel-case"), "alreadyCamelCase"); 60 + assertEquals(sanitizeIdentifier("single"), "single"); 61 + assertEquals(sanitizeIdentifier("with_underscore"), "withUnderscore"); 62 + assertEquals(sanitizeIdentifier("mixed-name_test"), "mixedNameTest"); 63 }); 64 65 Deno.test("isPropertyRequired - checks if property is required", () => {