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 1 import type { SourceFile } from "ts-morph"; 2 2 import { Scope } from "ts-morph"; 3 3 import type { Lexicon } from "./mod.ts"; 4 - import { nsidToPascalCase, capitalizeFirst } from "./mod.ts"; 4 + import { nsidToPascalCase, capitalizeFirst, sanitizeIdentifier } from "./mod.ts"; 5 5 6 6 interface NestedStructure { 7 7 [key: string]: NestedStructure | string | QueryProcedureInfo[] | undefined; ··· 269 269 } 270 270 } else if (typeof value === "object" && Object.keys(value).length > 0) { 271 271 // Add nested property with PascalCase class name 272 - const nestedClassName = `${capitalizeFirst(key)}${className}`; 272 + // Sanitize the key for both class name and property name 273 + const sanitizedKey = sanitizeIdentifier(key); 274 + const nestedClassName = `${capitalizeFirst(sanitizedKey)}${className}`; 273 275 generateNestedClass(value as NestedStructure, nestedClassName, [ 274 276 ...currentPath, 275 277 key, 276 278 ]); 277 279 properties.push({ 278 - name: key, 280 + name: sanitizedKey, 279 281 type: nestedClassName, 280 282 }); 281 283 }
+26 -6
packages/codegen/src/mod.ts
··· 114 114 return str.charAt(0).toUpperCase() + str.slice(1); 115 115 } 116 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 + 117 128 // Check if property is required 118 129 export function isPropertyRequired( 119 130 recordObj: LexiconRecord, ··· 164 175 if (nonSlicesLexicon) { 165 176 // Use the first non-slices lexicon 166 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("."); 167 182 168 183 return `/** 169 184 * @example Usage ··· 176 191 * ); 177 192 * 178 193 * // Get records from the ${nonSlicesLexicon.id} collection 179 - * const records = await client.${parts.join(".")}.getRecords(); 194 + * const records = await client.${accessPath}.getRecords(); 180 195 * 181 196 * // Get a specific record 182 - * const record = await client.${parts.join(".")}.getRecord({ 197 + * const record = await client.${accessPath}.getRecord({ 183 198 * uri: 'at://did:plc:example/${nonSlicesLexicon.id}/3abc123' 184 199 * }); 185 200 * 186 201 * // Get records with filtering and search 187 - * const filteredRecords = await client.${parts.join(".")}.getRecords({ 202 + * const filteredRecords = await client.${accessPath}.getRecords({ 188 203 * where: { 189 204 * text: { contains: "example search term" } 190 205 * } ··· 225 240 226 241 if (anyRecordLexicon) { 227 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 + 228 248 return `/** 229 249 * @example Usage 230 250 * \`\`\`ts ··· 236 256 * ); 237 257 * 238 258 * // Get records from the ${anyRecordLexicon.id} collection 239 - * const records = await client.${parts.join(".")}.getRecords(); 259 + * const records = await client.${accessPath}.getRecords(); 240 260 * 241 261 * // Get a specific record 242 - * const record = await client.${parts.join(".")}.getRecord({ 262 + * const record = await client.${accessPath}.getRecord({ 243 263 * uri: 'at://did:plc:example/${anyRecordLexicon.id}/3abc123' 244 264 * }); 245 265 * 246 266 * // Get records with search and filtering 247 - * const filteredRecords = await client.${parts.join(".")}.getRecords({ 267 + * const filteredRecords = await client.${accessPath}.getRecords({ 248 268 * where: { 249 269 * text: { contains: "example search term" } 250 270 * }
+38
packages/codegen/tests/client_test.ts
··· 150 150 assertStringIncludes(result, "async createRecord("); 151 151 }); 152 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 + 153 191 Deno.test("generateClient - handles mixed lexicon types", () => { 154 192 const project = createTestProject(); 155 193 const sourceFile = project.createSourceFile("test.ts", "");
+10
packages/codegen/tests/utils_test.ts
··· 5 5 nsidToNamespace, 6 6 defNameToPascalCase, 7 7 capitalizeFirst, 8 + sanitizeIdentifier, 8 9 isPropertyRequired, 9 10 isFieldSortable, 10 11 createProject, ··· 50 51 assertEquals(capitalizeFirst("hello"), "Hello"); 51 52 assertEquals(capitalizeFirst("world"), "World"); 52 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"); 53 63 }); 54 64 55 65 Deno.test("isPropertyRequired - checks if property is required", () => {