gen-openapi.ts
1 import { writeFileSync } from 'fs'; 2 import https from 'https'; 3 import http from 'http'; 4 import { execSync } from 'child_process'; 5 import fs from 'fs'; 6 import path from 'path'; 7 8 const env = process.argv[2] ?? 'dev'; 9 const targets: Record<string, string> = { 10 dev: 'http://localhost:8000/openapi.json', 11 preview: 'https://api-preview.cognidao.org/openapi.json', 12 prod: 'https://api.cognidao.org/openapi.json' 13 }; 14 15 // Enhanced fetch function that follows redirects 16 function fetchData(url: string, maxRedirects: number = 5): Promise<string> { 17 return new Promise((resolve, reject) => { 18 const makeRequest = (currentUrl: string, redirectsLeft: number) => { 19 const client = currentUrl.startsWith('https') ? https : http; 20 21 console.log(` Fetching: ${currentUrl}`); 22 client.get(currentUrl, (res) => { 23 if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { 24 // Got a redirect 25 if (redirectsLeft === 0) { 26 reject(new Error(`Too many redirects (followed ${maxRedirects})`)); 27 return; 28 } 29 30 // Construct absolute URL if relative 31 const redirectUrl = /^https?:\/\//.test(res.headers.location) 32 ? res.headers.location 33 : new URL(res.headers.location, new URL(currentUrl).origin).toString(); 34 35 console.log(` Redirected to: ${redirectUrl}`); 36 makeRequest(redirectUrl, redirectsLeft - 1); 37 return; 38 } 39 40 if (res.statusCode !== 200) { 41 reject(new Error(`Failed to fetch: ${res.statusCode}`)); 42 return; 43 } 44 45 let data = ''; 46 res.on('data', (chunk) => { 47 data += chunk; 48 }); 49 50 res.on('end', () => { 51 resolve(data); 52 }); 53 }).on('error', (err) => { 54 reject(err); 55 }); 56 }; 57 58 makeRequest(url, maxRedirects); 59 }); 60 } 61 62 // Type definitions for schema handling 63 interface JsonSchema { 64 properties?: Record<string, any>; 65 required?: string[]; 66 description?: string; 67 [key: string]: any; 68 } 69 70 interface SchemaInfo { 71 type: string; 72 version: number | string; 73 latest_url?: string; 74 [key: string]: any; 75 } 76 77 // Generate combined metadata file with both Zod schema and interface 78 function generateCombinedMetadataFile(schema: JsonSchema, typeName: string): string { 79 // Generate properties for both Zod schema and TypeScript interface 80 const requiredProps = schema.required || []; 81 82 const zodProperties = Object.entries(schema.properties || {}).map(([propName, propSchema]) => { 83 const required = requiredProps.includes(propName); 84 let zodType = 'z.unknown()'; 85 86 // Handle different property types 87 if (propSchema.type === 'string') { 88 zodType = 'z.string()'; 89 if (propSchema.format === 'date-time') { 90 zodType = 'z.string().datetime()'; 91 } else if (propSchema.enum) { 92 zodType = `z.enum([${propSchema.enum.map(e => `'${e}'`).join(', ')}])`; 93 } 94 } else if (propSchema.type === 'number') { 95 zodType = 'z.number()'; 96 if (propSchema.minimum !== undefined) { 97 zodType = `${zodType}.min(${propSchema.minimum})`; 98 } 99 if (propSchema.maximum !== undefined) { 100 zodType = `${zodType}.max(${propSchema.maximum})`; 101 } 102 } else if (propSchema.type === 'boolean') { 103 zodType = 'z.boolean()'; 104 } else if (propSchema.type === 'array') { 105 zodType = 'z.array(z.unknown())'; 106 } else if (propSchema.type === 'object') { 107 zodType = 'z.record(z.string(), z.unknown())'; 108 } else if (propSchema.anyOf || propSchema.oneOf) { 109 // Handle union types 110 const types = (propSchema.anyOf || propSchema.oneOf).map((s: any) => { 111 if (s.type === 'null') return 'z.null()'; 112 if (s.type === 'string') return 'z.string()'; 113 if (s.type === 'number') return 'z.number()'; 114 if (s.type === 'boolean') return 'z.boolean()'; 115 return 'z.unknown()'; 116 }).join(', '); 117 zodType = `z.union([${types}])`; 118 } 119 120 return ` ${propName}: ${zodType}${required ? '' : '.optional()'}`; 121 }); 122 123 const tsProperties = Object.entries(schema.properties || {}).map(([propName, propSchema]) => { 124 const required = requiredProps.includes(propName); 125 let tsType = 'unknown'; 126 127 // Handle different property types 128 if (propSchema.type === 'string') { 129 tsType = 'string'; 130 if (propSchema.enum) { 131 tsType = propSchema.enum.map((e: string) => `'${e}'`).join(' | '); 132 } 133 } else if (propSchema.type === 'number') { 134 tsType = 'number'; 135 } else if (propSchema.type === 'boolean') { 136 tsType = 'boolean'; 137 } else if (propSchema.type === 'array') { 138 tsType = 'unknown[]'; 139 } else if (propSchema.type === 'object') { 140 tsType = 'Record<string, unknown>'; 141 } else if (propSchema.anyOf || propSchema.oneOf) { 142 // Handle union types 143 const types = (propSchema.anyOf || propSchema.oneOf).map((s: any) => { 144 if (s.type === 'null') return 'null'; 145 if (s.type === 'string') return 'string'; 146 if (s.type === 'number') return 'number'; 147 if (s.type === 'boolean') return 'boolean'; 148 return 'unknown'; 149 }).join(' | '); 150 tsType = types; 151 } 152 153 return ` ${propName}${required ? '' : '?'}: ${tsType};`; 154 }); 155 156 return `import { z } from 'zod'; 157 158 /** 159 * ${schema.description || `Metadata schema for ${typeName}`} 160 */ 161 export const ${typeName}Schema = z.object({ 162 ${zodProperties.join(',\n')} 163 }); 164 165 /** 166 * ${schema.description || `TypeScript interface for ${typeName}`} 167 * Manually defined to match Zod schema exactly (no z.infer<>) 168 */ 169 export interface ${typeName} { 170 ${tsProperties.join('\n')} 171 } 172 `; 173 } 174 175 // Main function to generate metadata models 176 async function generateMetadataModels(baseUrl: string): Promise<void> { 177 console.log('🧩 Generating metadata models...'); 178 179 try { 180 const outputDir = path.resolve(process.cwd(), 'src', 'data', 'block_metadata'); 181 182 // Ensure the output directory exists 183 if (!fs.existsSync(outputDir)) { 184 console.log(`Creating output directory: ${outputDir}`); 185 fs.mkdirSync(outputDir, { recursive: true }); 186 } 187 188 // Fetch schema index with the new API v1 path 189 const schemaIndexUrl = `${baseUrl}/api/v1/schemas/index.json`; 190 console.log(`Fetching schema index from: ${schemaIndexUrl}`); 191 192 const indexContent = await fetchData(schemaIndexUrl); 193 const schemaIndex = JSON.parse(indexContent).schemas as SchemaInfo[]; 194 195 if (!Array.isArray(schemaIndex)) { 196 throw new Error('Schema index does not contain a schemas array'); 197 } 198 199 console.log(`Found ${schemaIndex.length} schema entries`); 200 201 // Generate type map file 202 const typeMapEntries: string[] = []; 203 204 // Process each schema in the index 205 for (const schemaInfo of schemaIndex) { 206 const { type, version, latest_url } = schemaInfo; 207 208 // Skip base schema if it's not a specific block type 209 if (type === 'base') continue; 210 211 const capitalizedType = type.charAt(0).toUpperCase() + type.slice(1); 212 const typeName = `${capitalizedType}Metadata`; 213 214 // Use the latest_url from the API which already includes the /api/v1/ prefix 215 // or construct URL with the /api/v1/ prefix if not provided 216 const schemaUrl = latest_url ? 217 (latest_url.startsWith('http') ? latest_url : `${baseUrl}${latest_url}`) : 218 `${baseUrl}/api/v1/schemas/${type}/latest`; 219 220 console.log(`Fetching schema for ${type} from: ${schemaUrl}`); 221 222 try { 223 const schemaContent = await fetchData(schemaUrl); 224 const schema = JSON.parse(schemaContent); 225 226 // Generate combined file with both Zod schema and TypeScript interface 227 const combinedContent = generateCombinedMetadataFile(schema, typeName); 228 const filePath = path.join(outputDir, `${type}.ts`); 229 fs.writeFileSync(filePath, combinedContent, 'utf-8'); 230 console.log(`Generated combined schema file: ${filePath}`); 231 232 // Add to type map 233 typeMapEntries.push(` ${type}: ${typeName};`); 234 } catch (error) { 235 console.error(`Failed to generate schema for ${type}:`, error); 236 } 237 } 238 239 // Generate the BlockMetadataByType map 240 if (typeMapEntries.length > 0) { 241 const typeMapContent = `import { MemoryBlockType } from '@/data/models/memoryBlockType'; 242 ${schemaIndex 243 .filter(s => s.type !== 'base') 244 .map(s => { 245 const capitalizedType = s.type.charAt(0).toUpperCase() + s.type.slice(1); 246 return `import type { ${capitalizedType}Metadata } from './${s.type}';`; 247 }) 248 .join('\n')} 249 250 /** 251 * Type map to help with narrowing metadata types based on block type 252 */ 253 export interface BlockMetadataByType { 254 ${typeMapEntries.join('\n')} 255 } 256 257 /** 258 * Helper function to narrow metadata type based on block type 259 */ 260 export function narrowMetadata<T extends MemoryBlockType>( 261 blockType: T, 262 metadata: unknown 263 ): BlockMetadataByType[T] { 264 return metadata as BlockMetadataByType[T]; 265 } 266 `; 267 const typeMapPath = path.join(outputDir, 'index.ts'); 268 fs.writeFileSync(typeMapPath, typeMapContent, 'utf-8'); 269 console.log(`Generated BlockMetadataByType map: ${typeMapPath}`); 270 } 271 272 console.log('✅ Metadata model generation complete!'); 273 } catch (error) { 274 console.error('Error generating metadata models:', error); 275 throw error; 276 } 277 } 278 279 // Main function to orchestrate the generation 280 (async () => { 281 const url = targets[env]; 282 if (!url) throw new Error(`Unknown env ${env}`); 283 const baseUrl = new URL(url).origin; 284 285 console.log(`📥 Fetching OpenAPI from ${url}`); 286 try { 287 const spec = await fetchData(url); 288 289 // Create schemas directory if it doesn't exist 290 execSync('mkdir -p schemas'); 291 292 writeFileSync('schemas/openapi.json', spec); 293 console.log('✅ Saved schemas/openapi.json'); 294 295 console.log('🛠 Generating TypeScript types & component schemas'); 296 execSync('mkdir -p src/types'); 297 execSync('npx openapi-typescript schemas/openapi.json -o src/types/api.d.ts', { 298 stdio: 'inherit' 299 }); 300 301 console.log('🏗 Running Orval to generate API client'); 302 execSync('npm run gen:api', { 303 stdio: 'inherit' 304 }); 305 306 // Generate metadata models 307 await generateMetadataModels(baseUrl); 308 309 console.log('✨ All generation complete'); 310 } catch (error) { 311 console.error('Error:', error); 312 process.exit(1); 313 } 314 })();