openapi-domainify 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,808 @@
1
+ // src/types.ts
2
+ function defineConfig(config) {
3
+ return config;
4
+ }
5
+
6
+ // src/config.ts
7
+ import { existsSync } from "fs";
8
+ import { pathToFileURL } from "url";
9
+ import { resolve } from "path";
10
+ var CONFIG_FILES = [
11
+ "domainify.config.ts",
12
+ "domainify.config.js",
13
+ "domainify.config.mjs"
14
+ ];
15
+ async function loadConfig(configPath) {
16
+ let resolvedPath;
17
+ if (configPath) {
18
+ resolvedPath = resolve(configPath);
19
+ if (!existsSync(resolvedPath)) {
20
+ throw new Error(`Config file not found: ${configPath}`);
21
+ }
22
+ } else {
23
+ for (const file of CONFIG_FILES) {
24
+ const fullPath = resolve(process.cwd(), file);
25
+ if (existsSync(fullPath)) {
26
+ resolvedPath = fullPath;
27
+ break;
28
+ }
29
+ }
30
+ }
31
+ if (!resolvedPath) {
32
+ throw new Error(
33
+ `No config file found. Create one of: ${CONFIG_FILES.join(", ")}
34
+ Or run: npx openapi-domainify init`
35
+ );
36
+ }
37
+ const configModule = await import(pathToFileURL(resolvedPath).href);
38
+ const config = configModule.default;
39
+ if (!config.input) {
40
+ throw new Error("Config missing required field: input");
41
+ }
42
+ if (!config.output) {
43
+ throw new Error("Config missing required field: output");
44
+ }
45
+ if (!config.domains || config.domains.length === 0) {
46
+ throw new Error("Config missing required field: domains (must have at least one)");
47
+ }
48
+ return resolveConfig(config);
49
+ }
50
+ function resolveConfig(config) {
51
+ return {
52
+ ...config,
53
+ fallback: config.fallback ?? { name: "api", prefix: "/", className: "Api" },
54
+ httpClientImport: config.httpClientImport ?? "../../http",
55
+ overridesDir: config.overridesDir ?? "./overrides",
56
+ generateIndex: config.generateIndex ?? true
57
+ };
58
+ }
59
+
60
+ // src/generator.ts
61
+ import { writeFileSync, mkdirSync, existsSync as existsSync2, rmSync, readFileSync } from "fs";
62
+ import { join, dirname, resolve as resolve2 } from "path";
63
+ import { execSync } from "child_process";
64
+ import * as ts from "typescript";
65
+ async function loadSpec(config) {
66
+ const { input } = config;
67
+ if (input.startsWith("http://") || input.startsWith("https://")) {
68
+ console.log("Fetching OpenAPI spec...");
69
+ console.log(` Source: ${input}`);
70
+ const response = await fetch(input);
71
+ if (!response.ok) {
72
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
73
+ }
74
+ const text = await response.text();
75
+ console.log(` Fetched successfully`);
76
+ return text;
77
+ }
78
+ const filePath = resolve2(input);
79
+ if (!existsSync2(filePath)) {
80
+ throw new Error(`OpenAPI spec not found: ${filePath}`);
81
+ }
82
+ console.log("Loading OpenAPI spec...");
83
+ console.log(` Source: ${filePath}`);
84
+ return readFileSync(filePath, "utf-8");
85
+ }
86
+ function parseSpec(content) {
87
+ try {
88
+ return JSON.parse(content);
89
+ } catch {
90
+ throw new Error(
91
+ "Failed to parse OpenAPI spec. Currently only JSON format is supported.\nIf you have a YAML spec, convert it to JSON first."
92
+ );
93
+ }
94
+ }
95
+ function generateOpenapiTypes(specPath, outputPath) {
96
+ console.log("\nGenerating TypeScript types from OpenAPI spec...");
97
+ try {
98
+ execSync(`npx openapi-typescript "${specPath}" -o "${outputPath}"`, {
99
+ stdio: "pipe",
100
+ encoding: "utf-8"
101
+ });
102
+ console.log(` Generated base types`);
103
+ } catch (error) {
104
+ const message = error instanceof Error ? error.message : String(error);
105
+ throw new Error(`Failed to generate types: ${message}`);
106
+ }
107
+ }
108
+ function createProgram2(openapiTsPath) {
109
+ if (!existsSync2(openapiTsPath)) {
110
+ throw new Error(`Generated types not found: ${openapiTsPath}`);
111
+ }
112
+ const program = ts.createProgram([openapiTsPath], {
113
+ target: ts.ScriptTarget.ESNext,
114
+ module: ts.ModuleKind.ESNext,
115
+ strict: true
116
+ });
117
+ const checker = program.getTypeChecker();
118
+ const sourceFile = program.getSourceFile(openapiTsPath);
119
+ if (!sourceFile) {
120
+ throw new Error(`Could not load ${openapiTsPath}`);
121
+ }
122
+ let pathsSymbol;
123
+ ts.forEachChild(sourceFile, (node) => {
124
+ if (ts.isInterfaceDeclaration(node) && node.name.text === "paths") {
125
+ pathsSymbol = checker.getSymbolAtLocation(node.name);
126
+ }
127
+ });
128
+ if (!pathsSymbol) {
129
+ throw new Error('Could not find "paths" interface in generated types');
130
+ }
131
+ const pathsType = checker.getDeclaredTypeOfSymbol(pathsSymbol);
132
+ return { checker, sourceFile, pathsType };
133
+ }
134
+ function extractEndpoints(checker, sourceFile, pathsType) {
135
+ const endpoints = [];
136
+ const methods = ["get", "post", "put", "patch", "delete"];
137
+ for (const pathProp of pathsType.getProperties()) {
138
+ const path = pathProp.name;
139
+ const pathType = checker.getTypeOfSymbolAtLocation(pathProp, sourceFile);
140
+ for (const method of methods) {
141
+ const methodProp = pathType.getProperty(method);
142
+ if (!methodProp) continue;
143
+ const methodType = checker.getTypeOfSymbolAtLocation(methodProp, sourceFile);
144
+ const typeString = checker.typeToString(methodType);
145
+ if (typeString === "undefined" || typeString === "never") continue;
146
+ const operationType = checker.getApparentType(methodType);
147
+ const pathParams = (path.match(/\{([^}]+)\}/g) || []).map((p) => p.slice(1, -1));
148
+ const queryParams = [];
149
+ const parametersProp = operationType.getProperty("parameters");
150
+ if (parametersProp) {
151
+ const paramsType = checker.getApparentType(
152
+ checker.getTypeOfSymbolAtLocation(parametersProp, sourceFile)
153
+ );
154
+ const queryProp = paramsType.getProperty("query");
155
+ if (queryProp) {
156
+ const queryType = checker.getTypeOfSymbolAtLocation(queryProp, sourceFile);
157
+ for (const prop of queryType.getProperties()) {
158
+ queryParams.push(prop.name);
159
+ }
160
+ }
161
+ }
162
+ const requestBodyProp = operationType.getProperty("requestBody");
163
+ let hasBody = false;
164
+ if (requestBodyProp) {
165
+ const bodyType = checker.getTypeOfSymbolAtLocation(requestBodyProp, sourceFile);
166
+ hasBody = !(bodyType.flags & (ts.TypeFlags.Never | ts.TypeFlags.Undefined));
167
+ }
168
+ endpoints.push({
169
+ path,
170
+ method,
171
+ pathParams,
172
+ queryParams,
173
+ hasBody
174
+ });
175
+ }
176
+ }
177
+ return endpoints;
178
+ }
179
+ function createTypeExtractor(checker, sourceFile, pathsType) {
180
+ const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
181
+ function formatType(type) {
182
+ const typeNode = checker.typeToTypeNode(
183
+ type,
184
+ sourceFile,
185
+ ts.NodeBuilderFlags.NoTruncation | ts.NodeBuilderFlags.MultilineObjectLiterals
186
+ );
187
+ if (!typeNode) {
188
+ return checker.typeToString(type, sourceFile, ts.TypeFormatFlags.NoTruncation);
189
+ }
190
+ return printer.printNode(ts.EmitHint.Unspecified, typeNode, sourceFile);
191
+ }
192
+ function getNestedType(type, ...propertyPath) {
193
+ let currentType = type;
194
+ for (const prop of propertyPath) {
195
+ currentType = checker.getApparentType(currentType);
196
+ const property = currentType.getProperty(prop);
197
+ if (!property) return void 0;
198
+ currentType = checker.getTypeOfSymbolAtLocation(property, sourceFile);
199
+ }
200
+ return checker.getApparentType(currentType);
201
+ }
202
+ function getMethodType(path, method) {
203
+ const pathProp = pathsType.getProperty(path);
204
+ if (!pathProp) return void 0;
205
+ const pathType = checker.getTypeOfSymbolAtLocation(pathProp, sourceFile);
206
+ const methodProp = pathType.getProperty(method);
207
+ if (!methodProp) return void 0;
208
+ return checker.getApparentType(checker.getTypeOfSymbolAtLocation(methodProp, sourceFile));
209
+ }
210
+ function isUnknownType(typeStr) {
211
+ return typeStr === "unknown" || typeStr === "never" || typeStr === "undefined";
212
+ }
213
+ return {
214
+ getResponseType(path, method) {
215
+ const methodType = getMethodType(path, method);
216
+ if (!methodType) return null;
217
+ const responseType = getNestedType(methodType, "responses", "200", "content", "application/json");
218
+ if (!responseType) return null;
219
+ const formatted = formatType(responseType);
220
+ return isUnknownType(formatted) ? null : formatted;
221
+ },
222
+ getRequestType(path, method) {
223
+ const methodType = getMethodType(path, method);
224
+ if (!methodType) return null;
225
+ const requestBodyProp = methodType.getProperty("requestBody");
226
+ if (!requestBodyProp) return null;
227
+ let requestBodyType = checker.getTypeOfSymbolAtLocation(requestBodyProp, sourceFile);
228
+ if (requestBodyType.isUnion()) {
229
+ const nonUndefinedTypes = requestBodyType.types.filter(
230
+ (t) => !(t.flags & ts.TypeFlags.Undefined)
231
+ );
232
+ if (nonUndefinedTypes.length === 1) {
233
+ requestBodyType = nonUndefinedTypes[0];
234
+ }
235
+ }
236
+ requestBodyType = checker.getApparentType(requestBodyType);
237
+ const requestType = getNestedType(requestBodyType, "content", "application/json");
238
+ if (!requestType) return null;
239
+ const formatted = formatType(requestType);
240
+ return isUnknownType(formatted) ? null : formatted;
241
+ },
242
+ getQueryType(path, method) {
243
+ const methodType = getMethodType(path, method);
244
+ if (!methodType) return null;
245
+ const queryType = getNestedType(methodType, "parameters", "query");
246
+ if (!queryType) return null;
247
+ const formatted = formatType(queryType);
248
+ return isUnknownType(formatted) ? null : formatted;
249
+ }
250
+ };
251
+ }
252
+ function getAllDomains(config) {
253
+ return [...config.domains, config.fallback];
254
+ }
255
+ function getDomain(path, config) {
256
+ const strippedPath = config.stripPrefix ? path.replace(config.stripPrefix, "") : path;
257
+ for (const domain of config.domains) {
258
+ if (strippedPath.startsWith(domain.prefix)) {
259
+ return domain.name;
260
+ }
261
+ }
262
+ return config.fallback.name;
263
+ }
264
+ function getDomainConfig(name, config) {
265
+ const found = getAllDomains(config).find((d) => d.name === name);
266
+ if (!found) {
267
+ throw new Error(`Unknown domain: ${name}`);
268
+ }
269
+ return found;
270
+ }
271
+ function pascalCase(str) {
272
+ return str.replace(/[-_\/]([a-z])/g, (_, c) => c.toUpperCase()).replace(/^[a-z]/, (c) => c.toUpperCase()).replace(/[{}]/g, "");
273
+ }
274
+ function camelCase(str) {
275
+ return str.replace(/[-_\/]([a-z])/g, (_, c) => c.toUpperCase()).replace(/^[A-Z]/, (c) => c.toLowerCase()).replace(/[{}]/g, "");
276
+ }
277
+ function singularize(word) {
278
+ if (word.endsWith("ies")) return word.slice(0, -3) + "y";
279
+ if (/(x|ch|sh|ss|z)es$/i.test(word)) return word.slice(0, -2);
280
+ if (word.endsWith("s") && !word.endsWith("ss")) return word.slice(0, -1);
281
+ return word;
282
+ }
283
+ function getMethodName(endpoint, domain, config) {
284
+ const domainConfig = getDomainConfig(domain, config);
285
+ let relativePath = endpoint.path;
286
+ if (config.stripPrefix) {
287
+ relativePath = relativePath.replace(config.stripPrefix, "");
288
+ }
289
+ relativePath = relativePath.replace(domainConfig.prefix, "").replace(/^\//, "");
290
+ const allSegments = relativePath.split("/").filter(Boolean);
291
+ const endsWithParam = allSegments.length > 0 && allSegments[allSegments.length - 1].startsWith("{");
292
+ const segments = allSegments.filter((seg) => !seg.startsWith("{"));
293
+ if (segments.length === 0) {
294
+ return endpoint.method;
295
+ }
296
+ const resource = segments[0];
297
+ const subPath = segments.slice(1);
298
+ const resourceName = endsWithParam ? singularize(pascalCase(resource)) : pascalCase(resource);
299
+ const subPathPart = subPath.map(pascalCase).join("");
300
+ const fullPath = resourceName + subPathPart;
301
+ switch (endpoint.method) {
302
+ case "get":
303
+ if (endsWithParam || subPath.length > 0) {
304
+ return `get${fullPath}`;
305
+ }
306
+ return camelCase(fullPath);
307
+ case "post":
308
+ return `create${singularize(fullPath)}`;
309
+ case "put":
310
+ return `update${singularize(fullPath)}`;
311
+ case "patch":
312
+ return `patch${singularize(fullPath)}`;
313
+ case "delete":
314
+ return `delete${singularize(fullPath)}`;
315
+ default:
316
+ return `${endpoint.method}${fullPath}`;
317
+ }
318
+ }
319
+ function getTypeName(path, method, kind, config) {
320
+ let cleanPath = path;
321
+ if (config.stripPrefix) {
322
+ cleanPath = cleanPath.replace(config.stripPrefix, "");
323
+ }
324
+ cleanPath = cleanPath.replace(/^\//, "").replace(/\{[^}]+\}/g, "ById").split("/").map(pascalCase).join("");
325
+ const methodPrefix = method.charAt(0).toUpperCase() + method.slice(1);
326
+ return `${methodPrefix}${cleanPath}${kind}`;
327
+ }
328
+ function groupByDomain(endpoints, config) {
329
+ const groups = /* @__PURE__ */ new Map();
330
+ for (const domain of getAllDomains(config)) {
331
+ groups.set(domain.name, []);
332
+ }
333
+ for (const endpoint of endpoints) {
334
+ const domain = getDomain(endpoint.path, config);
335
+ const domainEndpoints = groups.get(domain);
336
+ if (domainEndpoints) {
337
+ domainEndpoints.push(endpoint);
338
+ }
339
+ }
340
+ return groups;
341
+ }
342
+ function generateTypes(domain, endpoints, extractor, config) {
343
+ const lines = [];
344
+ const domainConfig = getDomainConfig(domain, config);
345
+ const allTypes = [];
346
+ const requestTypes = [];
347
+ for (const endpoint of endpoints) {
348
+ if (endpoint.hasBody) {
349
+ const typeBody = extractor.getRequestType(endpoint.path, endpoint.method);
350
+ if (typeBody) {
351
+ const typeName = getTypeName(endpoint.path, endpoint.method, "Request", config);
352
+ requestTypes.push(`export type ${typeName} = ${typeBody}`);
353
+ }
354
+ }
355
+ }
356
+ allTypes.push(...requestTypes);
357
+ const queryTypes = [];
358
+ for (const endpoint of endpoints) {
359
+ if (endpoint.queryParams.length > 0) {
360
+ const typeBody = extractor.getQueryType(endpoint.path, endpoint.method);
361
+ if (typeBody) {
362
+ const typeName = getTypeName(endpoint.path, endpoint.method, "Query", config);
363
+ queryTypes.push(`export type ${typeName} = ${typeBody}`);
364
+ }
365
+ }
366
+ }
367
+ allTypes.push(...queryTypes);
368
+ const responseTypes = [];
369
+ for (const endpoint of endpoints) {
370
+ const typeBody = extractor.getResponseType(endpoint.path, endpoint.method);
371
+ if (typeBody) {
372
+ const typeName = getTypeName(endpoint.path, endpoint.method, "Response", config);
373
+ responseTypes.push(`export type ${typeName} = ${typeBody}`);
374
+ }
375
+ }
376
+ allTypes.push(...responseTypes);
377
+ const usesComponents = allTypes.some((t) => t.includes("components["));
378
+ lines.push(`/**`);
379
+ lines.push(` * ${domainConfig.className} Domain Types`);
380
+ lines.push(` * Auto-generated - DO NOT EDIT`);
381
+ lines.push(` */`);
382
+ if (usesComponents) {
383
+ lines.push(`import type { components } from '../openapi'`);
384
+ }
385
+ lines.push(``);
386
+ lines.push(`// \u2500\u2500\u2500 Common Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
387
+ lines.push(``);
388
+ lines.push(`export interface PaginatedResponse<T> {`);
389
+ lines.push(` data: T[]`);
390
+ lines.push(` current_page: number`);
391
+ lines.push(` last_page: number`);
392
+ lines.push(` per_page: number`);
393
+ lines.push(` total: number`);
394
+ lines.push(` from: number | null`);
395
+ lines.push(` to: number | null`);
396
+ lines.push(`}`);
397
+ lines.push(``);
398
+ if (requestTypes.length > 0) {
399
+ lines.push(`// \u2500\u2500\u2500 Request Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
400
+ lines.push(``);
401
+ lines.push(...requestTypes);
402
+ lines.push(``);
403
+ }
404
+ if (queryTypes.length > 0) {
405
+ lines.push(`// \u2500\u2500\u2500 Query Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
406
+ lines.push(``);
407
+ lines.push(...queryTypes);
408
+ lines.push(``);
409
+ }
410
+ if (responseTypes.length > 0) {
411
+ lines.push(`// \u2500\u2500\u2500 Response Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
412
+ lines.push(``);
413
+ lines.push(...responseTypes);
414
+ lines.push(``);
415
+ }
416
+ return lines.join("\n");
417
+ }
418
+ function generateService(domain, endpoints, extractor, config) {
419
+ const lines = [];
420
+ const domainConfig = getDomainConfig(domain, config);
421
+ const className = `${domainConfig.className}Service`;
422
+ lines.push(`/**`);
423
+ lines.push(` * ${domainConfig.className} Domain Service`);
424
+ lines.push(` * Auto-generated - DO NOT EDIT`);
425
+ lines.push(` */`);
426
+ lines.push(`import type { HttpClient } from '${config.httpClientImport}'`);
427
+ lines.push(`import type * as T from './types'`);
428
+ lines.push(``);
429
+ lines.push(`export class ${className} {`);
430
+ lines.push(` constructor(private http: HttpClient) {}`);
431
+ const usedNames = /* @__PURE__ */ new Map();
432
+ const resourceGroups = /* @__PURE__ */ new Map();
433
+ for (const endpoint of endpoints) {
434
+ let relativePath = endpoint.path;
435
+ if (config.stripPrefix) {
436
+ relativePath = relativePath.replace(config.stripPrefix, "");
437
+ }
438
+ relativePath = relativePath.replace(domainConfig.prefix, "").replace(/^\//, "");
439
+ const resource = relativePath.split("/")[0]?.replace(/\{[^}]+\}/, "") || "root";
440
+ if (!resourceGroups.has(resource)) resourceGroups.set(resource, []);
441
+ resourceGroups.get(resource).push(endpoint);
442
+ }
443
+ for (const [resource, resourceEndpoints] of resourceGroups) {
444
+ lines.push(``);
445
+ lines.push(` // \u2500\u2500\u2500 ${pascalCase(resource) || "Root"} \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
446
+ for (const endpoint of resourceEndpoints) {
447
+ let methodName = getMethodName(endpoint, domain, config);
448
+ const count = usedNames.get(methodName) || 0;
449
+ if (count > 0) {
450
+ methodName = `${methodName}${count + 1}`;
451
+ }
452
+ usedNames.set(methodName, count + 1);
453
+ const params = [];
454
+ for (const param of endpoint.pathParams) {
455
+ const paramName = camelCase(param.replace(/_id$/, "Id"));
456
+ params.push(`${paramName}: string | number`);
457
+ }
458
+ if (endpoint.queryParams.length > 0) {
459
+ const queryType = extractor.getQueryType(endpoint.path, endpoint.method);
460
+ if (queryType) {
461
+ const queryTypeName = getTypeName(endpoint.path, endpoint.method, "Query", config);
462
+ params.push(`params?: T.${queryTypeName}`);
463
+ } else {
464
+ params.push(`params?: Record<string, unknown>`);
465
+ }
466
+ }
467
+ if (endpoint.hasBody && endpoint.method !== "get") {
468
+ const requestType = extractor.getRequestType(endpoint.path, endpoint.method);
469
+ if (requestType) {
470
+ const bodyTypeName = getTypeName(endpoint.path, endpoint.method, "Request", config);
471
+ params.push(`data: T.${bodyTypeName}`);
472
+ } else {
473
+ params.push(`data: unknown`);
474
+ }
475
+ }
476
+ const responseType = extractor.getResponseType(endpoint.path, endpoint.method);
477
+ let returnType;
478
+ if (responseType) {
479
+ const responseTypeName = getTypeName(endpoint.path, endpoint.method, "Response", config);
480
+ returnType = `T.${responseTypeName}`;
481
+ } else {
482
+ returnType = "unknown";
483
+ }
484
+ let urlPath = endpoint.path;
485
+ if (config.stripPrefix) {
486
+ urlPath = urlPath.replace(config.stripPrefix, "");
487
+ }
488
+ for (const param of endpoint.pathParams) {
489
+ const paramName = camelCase(param.replace(/_id$/, "Id"));
490
+ urlPath = urlPath.replace(`{${param}}`, `\${${paramName}}`);
491
+ }
492
+ const pathLiteral = urlPath.includes("$") ? "`" + urlPath + "`" : `'${urlPath}'`;
493
+ lines.push(` ${methodName}(${params.join(", ")}): Promise<${returnType}> {`);
494
+ const hasParams = endpoint.queryParams.length > 0;
495
+ switch (endpoint.method) {
496
+ case "get":
497
+ lines.push(hasParams ? ` return this.http.get(${pathLiteral}, { params })` : ` return this.http.get(${pathLiteral})`);
498
+ break;
499
+ case "post":
500
+ lines.push(endpoint.hasBody ? ` return this.http.post(${pathLiteral}, data)` : ` return this.http.post(${pathLiteral})`);
501
+ break;
502
+ case "put":
503
+ lines.push(endpoint.hasBody ? ` return this.http.put(${pathLiteral}, data)` : ` return this.http.put(${pathLiteral})`);
504
+ break;
505
+ case "patch":
506
+ lines.push(endpoint.hasBody ? ` return this.http.patch(${pathLiteral}, data)` : ` return this.http.patch(${pathLiteral})`);
507
+ break;
508
+ case "delete":
509
+ lines.push(hasParams ? ` return this.http.delete(${pathLiteral}, { params })` : ` return this.http.delete(${pathLiteral})`);
510
+ break;
511
+ }
512
+ lines.push(` }`);
513
+ }
514
+ }
515
+ lines.push(`}`);
516
+ lines.push(``);
517
+ return lines.join("\n");
518
+ }
519
+ function generateOverrideTemplate(domain, config) {
520
+ const domainConfig = getDomainConfig(domain, config);
521
+ const className = `${domainConfig.className}Service`;
522
+ return `/**
523
+ * ${domainConfig.className} Service Overrides
524
+ *
525
+ * Extend or override methods from the generated ${className}.
526
+ * This file is NOT overwritten by the generator - safe to edit.
527
+ *
528
+ * To enable: update index.ts to import from here instead of generated.
529
+ */
530
+ import { ${className} as Generated${className} } from '../../generated/${domain}/service'
531
+ import type { HttpClient } from '../../http'
532
+
533
+ export class ${className} extends Generated${className} {
534
+ constructor(http: HttpClient) {
535
+ super(http)
536
+ }
537
+
538
+ // Example: Override a method that has issues in the OpenAPI spec
539
+ // async someMethod(): Promise<SomeType> {
540
+ // // Custom implementation or fix
541
+ // return super.someMethod()
542
+ // }
543
+
544
+ // Example: Add a method not in the OpenAPI spec
545
+ // async customMethod(): Promise<void> {
546
+ // // Custom logic
547
+ // }
548
+ }
549
+ `;
550
+ }
551
+ function generateHttpTemplate(config) {
552
+ return `/**
553
+ * HTTP Client Interface
554
+ *
555
+ * Implement this interface with your preferred HTTP library.
556
+ * This file is NOT overwritten by the generator - safe to edit.
557
+ */
558
+
559
+ export interface HttpClient {
560
+ get<T>(url: string, options?: { params?: Record<string, unknown> }): Promise<T>
561
+ post<T>(url: string, body?: Record<string, unknown>): Promise<T>
562
+ put<T>(url: string, body?: Record<string, unknown>): Promise<T>
563
+ patch<T>(url: string, body?: Record<string, unknown>): Promise<T>
564
+ delete<T>(url: string, options?: { params?: Record<string, unknown> }): Promise<T>
565
+ }
566
+
567
+ // \u2500\u2500\u2500 Example Implementation (using fetch) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
568
+
569
+ export interface HttpClientOptions {
570
+ baseUrl: string
571
+ getToken?: () => string | null | Promise<string | null>
572
+ onError?: (error: ApiError) => void
573
+ }
574
+
575
+ export interface ApiError {
576
+ status: number
577
+ statusText: string
578
+ message: string
579
+ data?: unknown
580
+ }
581
+
582
+ export class FetchHttpClient implements HttpClient {
583
+ constructor(private options: HttpClientOptions) {}
584
+
585
+ private async request<T>(
586
+ method: string,
587
+ url: string,
588
+ body?: unknown,
589
+ params?: Record<string, unknown>
590
+ ): Promise<T> {
591
+ const query = params
592
+ ? '?' + new URLSearchParams(
593
+ Object.entries(params)
594
+ .filter(([, v]) => v !== undefined)
595
+ .map(([k, v]) => [k, String(v)])
596
+ ).toString()
597
+ : ''
598
+
599
+ const token = await this.options.getToken?.()
600
+
601
+ const response = await fetch(this.options.baseUrl + url + query, {
602
+ method,
603
+ headers: {
604
+ 'Content-Type': 'application/json',
605
+ ...(token && { Authorization: \`Bearer \${token}\` }),
606
+ },
607
+ body: body ? JSON.stringify(body) : undefined,
608
+ })
609
+
610
+ if (!response.ok) {
611
+ const error: ApiError = {
612
+ status: response.status,
613
+ statusText: response.statusText,
614
+ message: response.statusText,
615
+ }
616
+ try {
617
+ error.data = await response.json()
618
+ error.message = (error.data as { message?: string })?.message || error.statusText
619
+ } catch {}
620
+ this.options.onError?.(error)
621
+ throw error
622
+ }
623
+
624
+ return response.json()
625
+ }
626
+
627
+ get<T>(url: string, opts?: { params?: Record<string, unknown> }) {
628
+ return this.request<T>('GET', url, undefined, opts?.params)
629
+ }
630
+ post<T>(url: string, body?: Record<string, unknown>) {
631
+ return this.request<T>('POST', url, body)
632
+ }
633
+ put<T>(url: string, body?: Record<string, unknown>) {
634
+ return this.request<T>('PUT', url, body)
635
+ }
636
+ patch<T>(url: string, body?: Record<string, unknown>) {
637
+ return this.request<T>('PATCH', url, body)
638
+ }
639
+ delete<T>(url: string, opts?: { params?: Record<string, unknown> }) {
640
+ return this.request<T>('DELETE', url, undefined, opts?.params)
641
+ }
642
+ }
643
+ `;
644
+ }
645
+ function generateIndexFile(activeDomains, config, overridesDir) {
646
+ const lines = [];
647
+ lines.push(`/**`);
648
+ lines.push(` * API Client`);
649
+ lines.push(` *`);
650
+ lines.push(` * Auto-generated index that wires domain services together.`);
651
+ lines.push(` * This file IS overwritten by the generator.`);
652
+ lines.push(` *`);
653
+ lines.push(` * To use overrides: uncomment the override import and comment out the generated import.`);
654
+ lines.push(` */`);
655
+ lines.push(`import type { HttpClient } from './http'`);
656
+ lines.push(``);
657
+ for (const domain of activeDomains) {
658
+ const domainConfig = getDomainConfig(domain, config);
659
+ const className = `${domainConfig.className}Service`;
660
+ const overridePath = join(overridesDir, domain, "service.ts");
661
+ const hasOverride = existsSync2(overridePath);
662
+ if (hasOverride) {
663
+ const content = readFileSync(overridePath, "utf-8");
664
+ const isCustomized = !content.includes("// Example: Override a method") || content.split("async ").length > 2;
665
+ if (isCustomized) {
666
+ lines.push(`import { ${className} } from './overrides/${domain}/service'`);
667
+ } else {
668
+ lines.push(`// import { ${className} } from './overrides/${domain}/service'`);
669
+ lines.push(`import { ${className} } from './generated/${domain}/service'`);
670
+ }
671
+ } else {
672
+ lines.push(`import { ${className} } from './generated/${domain}/service'`);
673
+ }
674
+ }
675
+ lines.push(``);
676
+ lines.push(`// \u2500\u2500\u2500 API Client \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
677
+ lines.push(``);
678
+ lines.push(`export class ApiClient {`);
679
+ lines.push(` constructor(private http: HttpClient) {`);
680
+ for (const domain of activeDomains) {
681
+ const domainConfig = getDomainConfig(domain, config);
682
+ lines.push(` this.${domain} = new ${domainConfig.className}Service(http)`);
683
+ }
684
+ lines.push(` }`);
685
+ lines.push(``);
686
+ for (const domain of activeDomains) {
687
+ const domainConfig = getDomainConfig(domain, config);
688
+ lines.push(` readonly ${domain}: ${domainConfig.className}Service`);
689
+ }
690
+ lines.push(`}`);
691
+ lines.push(``);
692
+ lines.push(`export function createApiClient(http: HttpClient): ApiClient {`);
693
+ lines.push(` return new ApiClient(http)`);
694
+ lines.push(`}`);
695
+ lines.push(``);
696
+ lines.push(`// \u2500\u2500\u2500 Re-exports \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
697
+ lines.push(``);
698
+ for (const domain of activeDomains) {
699
+ const domainConfig = getDomainConfig(domain, config);
700
+ lines.push(`export * as ${domainConfig.className}Types from './generated/${domain}/types'`);
701
+ }
702
+ lines.push(``);
703
+ return lines.join("\n");
704
+ }
705
+ function writeFileSafe(path, content, overwrite = true) {
706
+ if (!overwrite && existsSync2(path)) {
707
+ return false;
708
+ }
709
+ writeFileSync(path, content);
710
+ return true;
711
+ }
712
+ async function generate(config) {
713
+ console.log("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
714
+ console.log("\u2551 openapi-domainify \u2551");
715
+ console.log("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D\n");
716
+ const specContent = await loadSpec(config);
717
+ const spec = parseSpec(specContent);
718
+ const outputDir = resolve2(config.output);
719
+ const generatedDir = join(outputDir, "generated");
720
+ const overridesDir = resolve2(outputDir, config.overridesDir);
721
+ if (!existsSync2(outputDir)) {
722
+ mkdirSync(outputDir, { recursive: true });
723
+ }
724
+ if (!existsSync2(generatedDir)) {
725
+ mkdirSync(generatedDir, { recursive: true });
726
+ }
727
+ const specPath = config.specOutput ? resolve2(config.specOutput) : join(generatedDir, "openapi.json");
728
+ const specDir = dirname(specPath);
729
+ if (!existsSync2(specDir)) {
730
+ mkdirSync(specDir, { recursive: true });
731
+ }
732
+ writeFileSync(specPath, JSON.stringify(spec, null, 2));
733
+ const openapiTsPath = join(generatedDir, "openapi.ts");
734
+ generateOpenapiTypes(specPath, openapiTsPath);
735
+ console.log("\n\u{1F4CA} Generating domain services...");
736
+ const { checker, sourceFile, pathsType } = createProgram2(openapiTsPath);
737
+ console.log(" Extracting endpoints...");
738
+ const endpoints = extractEndpoints(checker, sourceFile, pathsType);
739
+ console.log(` Found ${endpoints.length} endpoints`);
740
+ const extractor = createTypeExtractor(checker, sourceFile, pathsType);
741
+ const domains = groupByDomain(endpoints, config);
742
+ const activeDomains = [];
743
+ for (const [domain, domainEndpoints] of domains) {
744
+ if (domainEndpoints.length === 0) continue;
745
+ activeDomains.push(domain);
746
+ const domainDir = join(generatedDir, domain);
747
+ if (existsSync2(domainDir)) {
748
+ rmSync(domainDir, { recursive: true });
749
+ }
750
+ mkdirSync(domainDir, { recursive: true });
751
+ console.log(`
752
+ \u{1F4C1} generated/${domain}/ (${domainEndpoints.length} endpoints)`);
753
+ const typesContent = generateTypes(domain, domainEndpoints, extractor, config);
754
+ writeFileSync(join(domainDir, "types.ts"), typesContent);
755
+ console.log(` \u2728 types.ts`);
756
+ const serviceContent = generateService(domain, domainEndpoints, extractor, config);
757
+ writeFileSync(join(domainDir, "service.ts"), serviceContent);
758
+ console.log(` \u2728 service.ts`);
759
+ }
760
+ console.log("\n\u{1F3D7}\uFE0F Scaffolding...");
761
+ if (!existsSync2(overridesDir)) {
762
+ mkdirSync(overridesDir, { recursive: true });
763
+ }
764
+ for (const domain of activeDomains) {
765
+ const overrideDomainDir = join(overridesDir, domain);
766
+ if (!existsSync2(overrideDomainDir)) {
767
+ mkdirSync(overrideDomainDir, { recursive: true });
768
+ }
769
+ const overrideServicePath = join(overrideDomainDir, "service.ts");
770
+ if (writeFileSafe(overrideServicePath, generateOverrideTemplate(domain, config), false)) {
771
+ console.log(` \u{1F4DD} overrides/${domain}/service.ts (new)`);
772
+ }
773
+ }
774
+ const httpPath = join(outputDir, "http.ts");
775
+ if (writeFileSafe(httpPath, generateHttpTemplate(config), false)) {
776
+ console.log(` \u{1F4DD} http.ts (new)`);
777
+ }
778
+ if (config.generateIndex) {
779
+ const indexContent = generateIndexFile(activeDomains, config, overridesDir);
780
+ writeFileSync(join(outputDir, "index.ts"), indexContent);
781
+ console.log(` \u2728 index.ts`);
782
+ }
783
+ console.log("\n\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
784
+ console.log("\u2551 \u2705 Generation Complete! \u2551");
785
+ console.log("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
786
+ console.log(`
787
+ Output: ${outputDir}`);
788
+ console.log(`
789
+ Structure:`);
790
+ console.log(` ${outputDir}/`);
791
+ console.log(` \u251C\u2500\u2500 index.ts # API client (regenerated)`);
792
+ console.log(` \u251C\u2500\u2500 http.ts # HTTP client template (safe to edit)`);
793
+ console.log(` \u251C\u2500\u2500 generated/ # Auto-generated (DO NOT EDIT)`);
794
+ for (const domain of activeDomains) {
795
+ console.log(` \u2502 \u2514\u2500\u2500 ${domain}/`);
796
+ }
797
+ console.log(` \u2514\u2500\u2500 overrides/ # Your customizations (safe to edit)`);
798
+ for (const domain of activeDomains) {
799
+ console.log(` \u2514\u2500\u2500 ${domain}/`);
800
+ }
801
+ }
802
+ export {
803
+ defineConfig,
804
+ generate,
805
+ loadConfig,
806
+ resolveConfig
807
+ };
808
+ //# sourceMappingURL=index.js.map