openapi-remote-codegen 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/README.md +173 -0
- package/dist/config.d.ts +46 -0
- package/dist/config.js +45 -0
- package/dist/generators/api-client.d.ts +6 -0
- package/dist/generators/api-client.js +72 -0
- package/dist/generators/remote-functions.d.ts +3 -0
- package/dist/generators/remote-functions.js +439 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +88 -0
- package/dist/parser.d.ts +3 -0
- package/dist/parser.js +236 -0
- package/dist/public.d.ts +6 -0
- package/dist/public.js +5 -0
- package/dist/types.d.ts +44 -0
- package/dist/types.js +1 -0
- package/dist/utils/client-mapping.d.ts +6 -0
- package/dist/utils/client-mapping.js +9 -0
- package/dist/utils/naming.d.ts +26 -0
- package/dist/utils/naming.js +89 -0
- package/package.json +37 -0
- package/src/__tests__/client-mapping.test.ts +38 -0
- package/src/__tests__/config.test.ts +59 -0
- package/src/__tests__/naming.test.ts +68 -0
- package/src/__tests__/parser.test.ts +576 -0
- package/src/__tests__/remote-functions.test.ts +315 -0
- package/src/config.ts +95 -0
- package/src/generators/api-client.ts +82 -0
- package/src/generators/remote-functions.ts +521 -0
- package/src/index.ts +105 -0
- package/src/parser.ts +303 -0
- package/src/public.ts +7 -0
- package/src/types.ts +48 -0
- package/src/utils/client-mapping.ts +9 -0
- package/src/utils/naming.ts +99 -0
- package/tsconfig.json +16 -0
package/src/parser.ts
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import type { OpenAPIV3 } from 'openapi-types';
|
|
2
|
+
import type { OperationInfo, InlineRequestBody, ParsedSpec, ParameterInfo, RemoteType } from './types.js';
|
|
3
|
+
|
|
4
|
+
// Extended operation type to include our custom extensions
|
|
5
|
+
type OperationWithExtensions = OpenAPIV3.OperationObject & {
|
|
6
|
+
'x-remote-type'?: RemoteType;
|
|
7
|
+
'x-remote-invalidates'?: string[];
|
|
8
|
+
'x-client-property'?: string;
|
|
9
|
+
'x-remote-batch'?: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function parseOpenApiSpec(spec: OpenAPIV3.Document): ParsedSpec {
|
|
13
|
+
const operations: OperationInfo[] = [];
|
|
14
|
+
const tagsSet = new Set<string>();
|
|
15
|
+
|
|
16
|
+
for (const [path, pathItem] of Object.entries(spec.paths ?? {})) {
|
|
17
|
+
if (!pathItem) continue;
|
|
18
|
+
|
|
19
|
+
const methods = ['get', 'post', 'put', 'patch', 'delete'] as const;
|
|
20
|
+
|
|
21
|
+
for (const method of methods) {
|
|
22
|
+
const operation = pathItem[method] as OperationWithExtensions | undefined;
|
|
23
|
+
if (!operation) continue;
|
|
24
|
+
|
|
25
|
+
const remoteType = operation['x-remote-type'];
|
|
26
|
+
if (!remoteType) continue;
|
|
27
|
+
|
|
28
|
+
const tag = operation.tags?.[0] ?? 'Default';
|
|
29
|
+
tagsSet.add(tag);
|
|
30
|
+
|
|
31
|
+
const invalidates = operation['x-remote-invalidates'] ?? [];
|
|
32
|
+
|
|
33
|
+
const parameters = parseParameters(
|
|
34
|
+
operation.parameters ?? [],
|
|
35
|
+
pathItem.parameters ?? [],
|
|
36
|
+
spec.components
|
|
37
|
+
);
|
|
38
|
+
const requestBodyResult = parseRequestBody(operation.requestBody as OpenAPIV3.RequestBodyObject | undefined);
|
|
39
|
+
const requestBodySchema = requestBodyResult?.schema;
|
|
40
|
+
const isArrayBody = requestBodyResult?.isArray ?? false;
|
|
41
|
+
|
|
42
|
+
// Check success response codes in priority order (200, 201, 202)
|
|
43
|
+
const responseSchema =
|
|
44
|
+
parseResponse(operation.responses?.['200'] as OpenAPIV3.ResponseObject | undefined) ??
|
|
45
|
+
parseResponse(operation.responses?.['201'] as OpenAPIV3.ResponseObject | undefined) ??
|
|
46
|
+
parseResponse(operation.responses?.['202'] as OpenAPIV3.ResponseObject | undefined);
|
|
47
|
+
|
|
48
|
+
// Detect void response: commands with no JSON response body (204 No Content, or 200 with no body)
|
|
49
|
+
const isVoidResponse = !responseSchema && (remoteType === 'command' || remoteType === 'form');
|
|
50
|
+
const isBatch = remoteType === 'query' && (operation['x-remote-batch'] === true);
|
|
51
|
+
|
|
52
|
+
operations.push({
|
|
53
|
+
operationId: operation.operationId ?? `${method}_${path}`,
|
|
54
|
+
tag,
|
|
55
|
+
method,
|
|
56
|
+
path,
|
|
57
|
+
remoteType,
|
|
58
|
+
invalidates,
|
|
59
|
+
parameters,
|
|
60
|
+
requestBodySchema,
|
|
61
|
+
requestBodyRequired: requestBodyResult?.required,
|
|
62
|
+
isArrayBody,
|
|
63
|
+
inlineRequestBody: requestBodyResult?.inline,
|
|
64
|
+
responseSchema,
|
|
65
|
+
isVoidResponse,
|
|
66
|
+
isBatch,
|
|
67
|
+
summary: operation.summary,
|
|
68
|
+
clientPropertyName: operation['x-client-property'],
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
operations,
|
|
75
|
+
tags: Array.from(tagsSet),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function parseParameters(
|
|
80
|
+
opParams: (OpenAPIV3.ParameterObject | OpenAPIV3.ReferenceObject)[],
|
|
81
|
+
pathParams: (OpenAPIV3.ParameterObject | OpenAPIV3.ReferenceObject)[],
|
|
82
|
+
components?: OpenAPIV3.ComponentsObject
|
|
83
|
+
): ParameterInfo[] {
|
|
84
|
+
const allParams = [...pathParams, ...opParams];
|
|
85
|
+
const result: ParameterInfo[] = [];
|
|
86
|
+
|
|
87
|
+
for (const param of allParams) {
|
|
88
|
+
if ('$ref' in param) continue;
|
|
89
|
+
|
|
90
|
+
const schema = param.schema as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject | undefined;
|
|
91
|
+
const resolved = resolveSchema(schema, components);
|
|
92
|
+
|
|
93
|
+
const paramInfo: ParameterInfo = {
|
|
94
|
+
name: param.name,
|
|
95
|
+
in: param.in as 'query' | 'path' | 'header',
|
|
96
|
+
required: param.required ?? false,
|
|
97
|
+
type: getSchemaType(resolved),
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Check if the resolved schema is an enum
|
|
101
|
+
if (resolved?.enum) {
|
|
102
|
+
const enumName = findEnumName(schema, components);
|
|
103
|
+
if (enumName) {
|
|
104
|
+
paramInfo.enumName = enumName;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// For array parameters, capture the item type
|
|
109
|
+
if (resolved?.type === 'array' && resolved.items) {
|
|
110
|
+
const itemSchema = resolveSchema(
|
|
111
|
+
resolved.items as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject,
|
|
112
|
+
components
|
|
113
|
+
);
|
|
114
|
+
paramInfo.itemType = getSchemaType(itemSchema);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
result.push(paramInfo);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Resolve a schema through $ref and oneOf to get the underlying schema object.
|
|
125
|
+
*/
|
|
126
|
+
function resolveSchema(
|
|
127
|
+
schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject | undefined,
|
|
128
|
+
components?: OpenAPIV3.ComponentsObject
|
|
129
|
+
): OpenAPIV3.SchemaObject | undefined {
|
|
130
|
+
if (!schema) return undefined;
|
|
131
|
+
|
|
132
|
+
if ('$ref' in schema) {
|
|
133
|
+
const refName = schema.$ref.split('/').pop();
|
|
134
|
+
if (refName && components?.schemas?.[refName]) {
|
|
135
|
+
return components.schemas[refName] as OpenAPIV3.SchemaObject;
|
|
136
|
+
}
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Handle oneOf wrapping (NSwag nullable enums)
|
|
141
|
+
const schemaObj = schema as OpenAPIV3.SchemaObject;
|
|
142
|
+
if (schemaObj.oneOf) {
|
|
143
|
+
for (const item of schemaObj.oneOf) {
|
|
144
|
+
const resolved = resolveSchema(
|
|
145
|
+
item as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject,
|
|
146
|
+
components
|
|
147
|
+
);
|
|
148
|
+
if (resolved?.type || resolved?.enum) return resolved;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return schemaObj;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Find the enum type name from a schema that may be wrapped in oneOf/$ref.
|
|
157
|
+
*/
|
|
158
|
+
function findEnumName(
|
|
159
|
+
schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject | undefined,
|
|
160
|
+
components?: OpenAPIV3.ComponentsObject
|
|
161
|
+
): string | undefined {
|
|
162
|
+
if (!schema) return undefined;
|
|
163
|
+
|
|
164
|
+
if ('$ref' in schema) {
|
|
165
|
+
const refName = schema.$ref.split('/').pop();
|
|
166
|
+
if (refName && components?.schemas?.[refName]) {
|
|
167
|
+
const resolved = components.schemas[refName] as OpenAPIV3.SchemaObject;
|
|
168
|
+
if (resolved.enum) return refName;
|
|
169
|
+
}
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const schemaObj = schema as OpenAPIV3.SchemaObject;
|
|
174
|
+
if (schemaObj.oneOf) {
|
|
175
|
+
for (const item of schemaObj.oneOf) {
|
|
176
|
+
const name = findEnumName(
|
|
177
|
+
item as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject,
|
|
178
|
+
components
|
|
179
|
+
);
|
|
180
|
+
if (name) return name;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
interface RequestBodyParseResult {
|
|
188
|
+
schema: string;
|
|
189
|
+
isArray: boolean;
|
|
190
|
+
required: boolean;
|
|
191
|
+
inline?: InlineRequestBody;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function parseRequestBody(body: OpenAPIV3.RequestBodyObject | undefined): RequestBodyParseResult | undefined {
|
|
195
|
+
if (!body?.content?.['application/json']?.schema) return undefined;
|
|
196
|
+
|
|
197
|
+
const required = body.required ?? false;
|
|
198
|
+
const schema = body.content['application/json'].schema;
|
|
199
|
+
if ('$ref' in schema) {
|
|
200
|
+
const refName = schema.$ref.split('/').pop();
|
|
201
|
+
return refName ? { schema: `${refName}Schema`, isArray: false, required } : undefined;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Handle array request bodies (e.g., Treatment[])
|
|
205
|
+
const schemaObj = schema as OpenAPIV3.SchemaObject;
|
|
206
|
+
if (schemaObj.type === 'array' && schemaObj.items) {
|
|
207
|
+
const items = schemaObj.items as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject;
|
|
208
|
+
if ('$ref' in items) {
|
|
209
|
+
const itemName = items.$ref.split('/').pop();
|
|
210
|
+
return itemName ? { schema: `${itemName}Schema`, isArray: true, required } : undefined;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Handle inline object schemas (e.g., Dictionary<string, string> -> { type: "object", additionalProperties: ... })
|
|
215
|
+
if (schemaObj.type === 'object' && schemaObj.additionalProperties) {
|
|
216
|
+
const inline = resolveInlineObjectSchema(schemaObj);
|
|
217
|
+
if (inline) {
|
|
218
|
+
return { schema: '', isArray: false, required, inline };
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Convert an inline OpenAPI object schema with additionalProperties to a Zod schema and TS type.
|
|
227
|
+
* Handles Dictionary<TKey, TValue> patterns from C#.
|
|
228
|
+
*/
|
|
229
|
+
function resolveInlineObjectSchema(schema: OpenAPIV3.SchemaObject): InlineRequestBody | undefined {
|
|
230
|
+
const additionalProps = schema.additionalProperties;
|
|
231
|
+
if (!additionalProps || additionalProps === true) {
|
|
232
|
+
// additionalProperties: true or {} -> Record<string, unknown>
|
|
233
|
+
return {
|
|
234
|
+
zodSchema: 'z.record(z.string(), z.unknown())',
|
|
235
|
+
tsType: '{ [key: string]: any; }',
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (typeof additionalProps === 'object' && !('$ref' in additionalProps)) {
|
|
240
|
+
const valueSchema = additionalProps as OpenAPIV3.SchemaObject;
|
|
241
|
+
switch (valueSchema.type) {
|
|
242
|
+
case 'string':
|
|
243
|
+
return {
|
|
244
|
+
zodSchema: 'z.record(z.string(), z.string())',
|
|
245
|
+
tsType: '{ [key: string]: string; }',
|
|
246
|
+
};
|
|
247
|
+
case 'number':
|
|
248
|
+
case 'integer':
|
|
249
|
+
return {
|
|
250
|
+
zodSchema: 'z.record(z.string(), z.number())',
|
|
251
|
+
tsType: '{ [key: string]: number; }',
|
|
252
|
+
};
|
|
253
|
+
case 'boolean':
|
|
254
|
+
return {
|
|
255
|
+
zodSchema: 'z.record(z.string(), z.boolean())',
|
|
256
|
+
tsType: '{ [key: string]: boolean; }',
|
|
257
|
+
};
|
|
258
|
+
default:
|
|
259
|
+
return {
|
|
260
|
+
zodSchema: 'z.record(z.string(), z.unknown())',
|
|
261
|
+
tsType: '{ [key: string]: any; }',
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function parseResponse(response: OpenAPIV3.ResponseObject | undefined): string | undefined {
|
|
270
|
+
if (!response?.content?.['application/json']?.schema) return undefined;
|
|
271
|
+
|
|
272
|
+
const schema = response.content['application/json'].schema;
|
|
273
|
+
if ('$ref' in schema) {
|
|
274
|
+
return schema.$ref.split('/').pop();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Handle array responses (e.g., TrackerDefinitionDto[])
|
|
278
|
+
const schemaObj = schema as OpenAPIV3.SchemaObject;
|
|
279
|
+
if (schemaObj.type === 'array' && schemaObj.items) {
|
|
280
|
+
const items = schemaObj.items as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject;
|
|
281
|
+
if ('$ref' in items) {
|
|
282
|
+
const itemName = items.$ref.split('/').pop();
|
|
283
|
+
return itemName ? `${itemName}[]` : undefined;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function getSchemaType(schema: OpenAPIV3.SchemaObject | undefined): string {
|
|
291
|
+
if (!schema) return 'unknown';
|
|
292
|
+
|
|
293
|
+
if (schema.type === 'string') {
|
|
294
|
+
if (schema.format === 'uuid') return 'string';
|
|
295
|
+
if (schema.format === 'date-time') return 'Date';
|
|
296
|
+
return 'string';
|
|
297
|
+
}
|
|
298
|
+
if (schema.type === 'integer' || schema.type === 'number') return 'number';
|
|
299
|
+
if (schema.type === 'boolean') return 'boolean';
|
|
300
|
+
if (schema.type === 'array') return 'array';
|
|
301
|
+
|
|
302
|
+
return 'unknown';
|
|
303
|
+
}
|
package/src/public.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Public API for openapi-remote-codegen
|
|
2
|
+
export { defineConfig, resolveConfig } from './config.js';
|
|
3
|
+
export type { GeneratorConfig, UserConfig, ImportPaths, ErrorHandling } from './config.js';
|
|
4
|
+
export { parseOpenApiSpec } from './parser.js';
|
|
5
|
+
export type { ParsedSpec, OperationInfo, ParameterInfo, RemoteType, InlineRequestBody } from './types.js';
|
|
6
|
+
export { generateRemoteFunctions } from './generators/remote-functions.js';
|
|
7
|
+
export { generateApiClient } from './generators/api-client.js';
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export type RemoteType = 'query' | 'command' | 'form';
|
|
2
|
+
|
|
3
|
+
export interface ParameterInfo {
|
|
4
|
+
name: string;
|
|
5
|
+
in: 'query' | 'path' | 'header';
|
|
6
|
+
required: boolean;
|
|
7
|
+
type: string;
|
|
8
|
+
enumName?: string;
|
|
9
|
+
/** For array-type parameters, the type of each item (e.g., 'string', 'number') */
|
|
10
|
+
itemType?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Represents an inline request body that doesn't have a named $ref schema.
|
|
15
|
+
* Used for types like Dictionary<string, string> which become { type: "object", additionalProperties: ... }.
|
|
16
|
+
*/
|
|
17
|
+
export interface InlineRequestBody {
|
|
18
|
+
/** Zod schema expression (e.g., "z.record(z.string(), z.string())") */
|
|
19
|
+
zodSchema: string;
|
|
20
|
+
/** TypeScript type for the NSwag client cast (e.g., "{ [key: string]: string; }") */
|
|
21
|
+
tsType: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface OperationInfo {
|
|
25
|
+
operationId: string;
|
|
26
|
+
tag: string;
|
|
27
|
+
method: string;
|
|
28
|
+
path: string;
|
|
29
|
+
remoteType: RemoteType;
|
|
30
|
+
invalidates: string[];
|
|
31
|
+
parameters: ParameterInfo[];
|
|
32
|
+
requestBodySchema?: string;
|
|
33
|
+
requestBodyRequired?: boolean;
|
|
34
|
+
isArrayBody?: boolean;
|
|
35
|
+
/** Inline request body for schemas that don't have a named $ref (e.g., Dictionary<string, string>) */
|
|
36
|
+
inlineRequestBody?: InlineRequestBody;
|
|
37
|
+
responseSchema?: string;
|
|
38
|
+
isVoidResponse: boolean;
|
|
39
|
+
/** Whether this query should use query.batch() for N+1 prevention */
|
|
40
|
+
isBatch?: boolean;
|
|
41
|
+
summary?: string;
|
|
42
|
+
clientPropertyName?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ParsedSpec {
|
|
46
|
+
operations: OperationInfo[];
|
|
47
|
+
tags: string[];
|
|
48
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get the apiClient property name for an OpenAPI tag using camelCase fallback.
|
|
3
|
+
* Strips version prefixes (e.g., "V4 Trackers" -> "Trackers") and converts to camelCase.
|
|
4
|
+
* Used as fallback when no [ClientPropertyName] attribute is set on the controller.
|
|
5
|
+
*/
|
|
6
|
+
export function getClientPropertyName(tag: string): string {
|
|
7
|
+
const cleaned = tag.replace(/^V\d+\s*/i, '').replace(/\s+/g, '');
|
|
8
|
+
return cleaned.charAt(0).toLowerCase() + cleaned.slice(1);
|
|
9
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JavaScript reserved keywords that cannot be used as export names.
|
|
3
|
+
* Maps known API method names to safe alternatives.
|
|
4
|
+
*/
|
|
5
|
+
const RESERVED_KEYWORD_MAP: Record<string, string> = {
|
|
6
|
+
delete: 'remove',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const JS_RESERVED_KEYWORDS = new Set([
|
|
10
|
+
'delete', 'new', 'return', 'switch', 'throw', 'try', 'catch', 'finally',
|
|
11
|
+
'class', 'const', 'let', 'var', 'function', 'import', 'export', 'default',
|
|
12
|
+
'void', 'typeof', 'instanceof', 'in', 'of', 'if', 'else', 'for', 'while',
|
|
13
|
+
'do', 'break', 'continue', 'with', 'yield', 'await', 'async', 'super',
|
|
14
|
+
'this', 'null', 'undefined', 'true', 'false', 'enum', 'implements',
|
|
15
|
+
'interface', 'package', 'private', 'protected', 'public', 'static',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Ensure a function name is not a JavaScript reserved keyword.
|
|
20
|
+
* Uses explicit mapping for common cases (delete -> remove),
|
|
21
|
+
* falls back to underscore prefix for others.
|
|
22
|
+
*/
|
|
23
|
+
function safeExportName(name: string): string {
|
|
24
|
+
if (!JS_RESERVED_KEYWORDS.has(name)) return name;
|
|
25
|
+
return RESERVED_KEYWORD_MAP[name] ?? `_${name}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Convert operationId to function name.
|
|
30
|
+
* "Note_GetNotes" -> "getNotes"
|
|
31
|
+
* "Note_CreateNote" -> "createNote"
|
|
32
|
+
* "ClockFaces_Delete" -> "remove" (reserved keyword handling)
|
|
33
|
+
*/
|
|
34
|
+
export function operationIdToFunctionName(operationId: string): string {
|
|
35
|
+
const parts = operationId.split('_');
|
|
36
|
+
const name = parts.length > 1 ? parts.slice(1).join('_') : parts[0];
|
|
37
|
+
const functionName = name.charAt(0).toLowerCase() + name.slice(1);
|
|
38
|
+
return safeExportName(functionName);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Pluralize a single word.
|
|
43
|
+
*/
|
|
44
|
+
function pluralize(word: string): string {
|
|
45
|
+
const lower = word.toLowerCase();
|
|
46
|
+
if (lower.endsWith('s')) return lower;
|
|
47
|
+
if (lower.endsWith('y') && !/[aeiou]y$/.test(lower)) {
|
|
48
|
+
return lower.slice(0, -1) + 'ies';
|
|
49
|
+
}
|
|
50
|
+
if (/(?:sh|ch|x|z)$/.test(lower)) {
|
|
51
|
+
return lower + 'es';
|
|
52
|
+
}
|
|
53
|
+
return lower + 's';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Convert tag to camelCase file name with proper pluralization.
|
|
58
|
+
* "Note" -> "notes"
|
|
59
|
+
* "V4 Notes" -> "notes"
|
|
60
|
+
* "V4 Compression Lows" -> "compressionLows"
|
|
61
|
+
* "V1 Connector Status" -> "connectorStatus"
|
|
62
|
+
* "Battery" -> "batteries"
|
|
63
|
+
*/
|
|
64
|
+
export function tagToFileName(tag: string): string {
|
|
65
|
+
const cleaned = tag.replace(/^V\d+\s*/i, '').trim();
|
|
66
|
+
const words = cleaned.split(/\s+/);
|
|
67
|
+
|
|
68
|
+
// Pluralize the last word
|
|
69
|
+
const lastWord = pluralize(words[words.length - 1]);
|
|
70
|
+
const prefix = words.slice(0, -1);
|
|
71
|
+
|
|
72
|
+
// Build camelCase: first word lowercase, rest capitalized
|
|
73
|
+
const parts = [...prefix.map(w => w.toLowerCase()), lastWord];
|
|
74
|
+
return parts[0] + parts.slice(1).map(w => w.charAt(0).toUpperCase() + w.slice(1)).join('');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Resolve invalidation targets.
|
|
79
|
+
* Short names resolve within same tag, full operationIds are used as-is.
|
|
80
|
+
* "GetNotes" with currentTag "Note" -> "getNotes"
|
|
81
|
+
* "Trackers_GetActiveInstances" -> "getActiveInstances" (from trackers)
|
|
82
|
+
*/
|
|
83
|
+
export function resolveInvalidation(
|
|
84
|
+
invalidate: string,
|
|
85
|
+
currentTag: string
|
|
86
|
+
): { functionName: string; fromTag: string } {
|
|
87
|
+
if (invalidate.includes('_')) {
|
|
88
|
+
const [tag, ...rest] = invalidate.split('_');
|
|
89
|
+
return {
|
|
90
|
+
functionName: operationIdToFunctionName(invalidate),
|
|
91
|
+
fromTag: tag,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
functionName: invalidate.charAt(0).toLowerCase() + invalidate.slice(1),
|
|
97
|
+
fromTag: currentTag,
|
|
98
|
+
};
|
|
99
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"declaration": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*"],
|
|
15
|
+
"exclude": ["node_modules", "dist", "src/__tests__"]
|
|
16
|
+
}
|