@appweaver/client 1.0.23 → 1.0.24
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/cjs/package.json +3 -0
- package/{weaver-client.js → cjs/weaver-client.js} +1 -1
- package/esm/angular/index.d.ts +1 -0
- package/esm/angular/index.js +1 -0
- package/esm/clients/angular-client.d.ts +26 -0
- package/esm/clients/angular-client.js +53 -0
- package/esm/clients/base-client-interface.d.ts +86 -0
- package/esm/clients/base-client-interface.js +1 -0
- package/esm/clients/base-client.d.ts +216 -0
- package/esm/clients/base-client.js +242 -0
- package/esm/clients/fetch-client.d.ts +6 -0
- package/esm/clients/fetch-client.js +6 -0
- package/esm/clients/index.d.ts +4 -0
- package/esm/clients/index.js +4 -0
- package/esm/clients/modules/account-client.d.ts +69 -0
- package/esm/clients/modules/account-client.js +107 -0
- package/esm/clients/modules/auth-client.d.ts +56 -0
- package/esm/clients/modules/auth-client.js +74 -0
- package/esm/clients/modules/base-module.d.ts +17 -0
- package/esm/clients/modules/base-module.js +49 -0
- package/esm/clients/modules/files-client.d.ts +27 -0
- package/esm/clients/modules/files-client.js +42 -0
- package/esm/clients/modules/health-client.d.ts +25 -0
- package/esm/clients/modules/health-client.js +30 -0
- package/esm/clients/modules/index.d.ts +6 -0
- package/esm/clients/modules/index.js +6 -0
- package/esm/clients/modules/resource-client.d.ts +90 -0
- package/esm/clients/modules/resource-client.js +173 -0
- package/esm/clients/responses/file-data-response.d.ts +78 -0
- package/esm/clients/responses/file-data-response.js +105 -0
- package/esm/clients/responses/index.d.ts +1 -0
- package/esm/clients/responses/index.js +1 -0
- package/esm/commands/generate-command.d.ts +3 -0
- package/esm/commands/generate-command.js +122 -0
- package/esm/commands/index.d.ts +1 -0
- package/esm/commands/index.js +1 -0
- package/esm/constants.d.ts +67 -0
- package/esm/constants.js +94 -0
- package/esm/errors/client-error.d.ts +6 -0
- package/esm/errors/client-error.js +11 -0
- package/esm/errors/index.d.ts +1 -0
- package/esm/errors/index.js +1 -0
- package/esm/generators/generate-client.d.ts +16 -0
- package/esm/generators/generate-client.js +399 -0
- package/esm/generators/generate-types.d.ts +9 -0
- package/esm/generators/generate-types.js +486 -0
- package/esm/generators/index.d.ts +2 -0
- package/esm/generators/index.js +2 -0
- package/esm/index.d.ts +2 -0
- package/esm/index.js +2 -0
- package/esm/package.json +3 -0
- package/esm/types/index.d.ts +1 -0
- package/esm/types/index.js +1 -0
- package/esm/types/routes.d.ts +15 -0
- package/esm/types/routes.js +1 -0
- package/esm/utils/index.d.ts +1 -0
- package/esm/utils/index.js +1 -0
- package/esm/utils/schema-util.d.ts +3 -0
- package/esm/utils/schema-util.js +39 -0
- package/package.json +29 -5
- /package/{angular → cjs/angular}/index.d.ts +0 -0
- /package/{angular → cjs/angular}/index.js +0 -0
- /package/{clients → cjs/clients}/angular-client.d.ts +0 -0
- /package/{clients → cjs/clients}/angular-client.js +0 -0
- /package/{clients → cjs/clients}/base-client-interface.d.ts +0 -0
- /package/{clients → cjs/clients}/base-client-interface.js +0 -0
- /package/{clients → cjs/clients}/base-client.d.ts +0 -0
- /package/{clients → cjs/clients}/base-client.js +0 -0
- /package/{clients → cjs/clients}/fetch-client.d.ts +0 -0
- /package/{clients → cjs/clients}/fetch-client.js +0 -0
- /package/{clients → cjs/clients}/index.d.ts +0 -0
- /package/{clients → cjs/clients}/index.js +0 -0
- /package/{clients → cjs/clients}/modules/account-client.d.ts +0 -0
- /package/{clients → cjs/clients}/modules/account-client.js +0 -0
- /package/{clients → cjs/clients}/modules/auth-client.d.ts +0 -0
- /package/{clients → cjs/clients}/modules/auth-client.js +0 -0
- /package/{clients → cjs/clients}/modules/base-module.d.ts +0 -0
- /package/{clients → cjs/clients}/modules/base-module.js +0 -0
- /package/{clients → cjs/clients}/modules/files-client.d.ts +0 -0
- /package/{clients → cjs/clients}/modules/files-client.js +0 -0
- /package/{clients → cjs/clients}/modules/health-client.d.ts +0 -0
- /package/{clients → cjs/clients}/modules/health-client.js +0 -0
- /package/{clients → cjs/clients}/modules/index.d.ts +0 -0
- /package/{clients → cjs/clients}/modules/index.js +0 -0
- /package/{clients → cjs/clients}/modules/resource-client.d.ts +0 -0
- /package/{clients → cjs/clients}/modules/resource-client.js +0 -0
- /package/{clients → cjs/clients}/responses/file-data-response.d.ts +0 -0
- /package/{clients → cjs/clients}/responses/file-data-response.js +0 -0
- /package/{clients → cjs/clients}/responses/index.d.ts +0 -0
- /package/{clients → cjs/clients}/responses/index.js +0 -0
- /package/{commands → cjs/commands}/generate-command.d.ts +0 -0
- /package/{commands → cjs/commands}/generate-command.js +0 -0
- /package/{commands → cjs/commands}/index.d.ts +0 -0
- /package/{commands → cjs/commands}/index.js +0 -0
- /package/{constants.d.ts → cjs/constants.d.ts} +0 -0
- /package/{constants.js → cjs/constants.js} +0 -0
- /package/{errors → cjs/errors}/client-error.d.ts +0 -0
- /package/{errors → cjs/errors}/client-error.js +0 -0
- /package/{errors → cjs/errors}/index.d.ts +0 -0
- /package/{errors → cjs/errors}/index.js +0 -0
- /package/{generators → cjs/generators}/generate-client.d.ts +0 -0
- /package/{generators → cjs/generators}/generate-client.js +0 -0
- /package/{generators → cjs/generators}/generate-types.d.ts +0 -0
- /package/{generators → cjs/generators}/generate-types.js +0 -0
- /package/{generators → cjs/generators}/index.d.ts +0 -0
- /package/{generators → cjs/generators}/index.js +0 -0
- /package/{index.d.ts → cjs/index.d.ts} +0 -0
- /package/{index.js → cjs/index.js} +0 -0
- /package/{types → cjs/types}/index.d.ts +0 -0
- /package/{types → cjs/types}/index.js +0 -0
- /package/{types → cjs/types}/routes.d.ts +0 -0
- /package/{types → cjs/types}/routes.js +0 -0
- /package/{utils → cjs/utils}/index.d.ts +0 -0
- /package/{utils → cjs/utils}/index.js +0 -0
- /package/{utils → cjs/utils}/schema-util.d.ts +0 -0
- /package/{utils → cjs/utils}/schema-util.js +0 -0
- /package/{weaver-client.d.ts → cjs/weaver-client.d.ts} +0 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import openapiTS, { astToString } from 'openapi-typescript';
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
import { toSchemaObject } from '../utils/index.js';
|
|
4
|
+
import { ACCOUNT_MODULE_TYPE, ACCOUNT_TYPES, AUTH_MODULE_TYPE, AUTH_TYPES, CONFIG_FIELD, HEALTH_MODULE_TYPE, HEALTH_TYPES, RESOURCE_MODULE_TYPE, RESOURCE_TYPES } from '../constants.js';
|
|
5
|
+
/**
|
|
6
|
+
* Generates TypeScript types based on an OpenAPI V3 schema content.
|
|
7
|
+
*
|
|
8
|
+
* @param {string | OpenAPI3} schema - The OpenAPI V3 schema to generate types from. The value can be a string
|
|
9
|
+
* representing JSON or YAML format, or already parsed OpenAPI3 object.
|
|
10
|
+
* @return {Promise<string>} A promise that resolves to a string containing the generated TypeScript types.
|
|
11
|
+
*/
|
|
12
|
+
export async function generateTypes(schema) {
|
|
13
|
+
const schemaObject = typeof schema === 'string' ? await toSchemaObject(schema) : schema;
|
|
14
|
+
const ast = await openapiTS(schemaObject, {
|
|
15
|
+
exportType: true,
|
|
16
|
+
emptyObjectsUnknown: true,
|
|
17
|
+
enum: true,
|
|
18
|
+
enumValues: true,
|
|
19
|
+
transformProperty: (property, schemaObject) => {
|
|
20
|
+
const validationTags = [];
|
|
21
|
+
// Add validation JSDoc tags based on schema constraints
|
|
22
|
+
if (schemaObject['minLength'] !== undefined) {
|
|
23
|
+
validationTags.push(`@minLength ${schemaObject['minLength']}`);
|
|
24
|
+
}
|
|
25
|
+
if (schemaObject['maxLength'] !== undefined) {
|
|
26
|
+
validationTags.push(`@maxLength ${schemaObject['maxLength']}`);
|
|
27
|
+
}
|
|
28
|
+
if (schemaObject['minimum'] !== undefined) {
|
|
29
|
+
validationTags.push(`@minimum ${schemaObject['minimum']}`);
|
|
30
|
+
}
|
|
31
|
+
if (schemaObject['maximum'] !== undefined) {
|
|
32
|
+
validationTags.push(`@maximum ${schemaObject['maximum']}`);
|
|
33
|
+
}
|
|
34
|
+
if (schemaObject['pattern'] !== undefined) {
|
|
35
|
+
validationTags.push(`@pattern ${schemaObject['pattern']}`);
|
|
36
|
+
}
|
|
37
|
+
if (schemaObject['format'] !== undefined) {
|
|
38
|
+
validationTags.push(`@format ${schemaObject['format']}`);
|
|
39
|
+
}
|
|
40
|
+
if (validationTags.length > 0) {
|
|
41
|
+
const newProperty = ts.factory.updatePropertySignature(property, property.modifiers, property.name, property.questionToken, property.type);
|
|
42
|
+
const jsDocText = `*\n * ${validationTags.join('\n * ')}\n `;
|
|
43
|
+
ts.addSyntheticLeadingComment(newProperty, ts.SyntaxKind.MultiLineCommentTrivia, jsDocText, true);
|
|
44
|
+
return newProperty;
|
|
45
|
+
}
|
|
46
|
+
return property;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
let typesContent = astToString(deduplicateUnionConstituents(ast));
|
|
50
|
+
typesContent = extractSchemaTypes(typesContent);
|
|
51
|
+
typesContent = combineModuleTypes(typesContent, schemaObject);
|
|
52
|
+
typesContent = deduplicateExportedTypes(typesContent);
|
|
53
|
+
return replaceFileUploadTypes(typesContent);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Extracts inline schema types from the generated `schemas` block.
|
|
57
|
+
*
|
|
58
|
+
* Finds entries like:
|
|
59
|
+
* /** SomeTypeName *\/
|
|
60
|
+
* "def-N": { ... };
|
|
61
|
+
*
|
|
62
|
+
* Lifts each one into:
|
|
63
|
+
* export type SomeTypeName = { ... };
|
|
64
|
+
*
|
|
65
|
+
* And replaces the inline body in `schemas` with a reference to the new type.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} typeContent - The generated TypeScript type content as a string.
|
|
68
|
+
* @return {string} The transformed types content with extracted schema types.
|
|
69
|
+
*/
|
|
70
|
+
function extractSchemaTypes(typeContent) {
|
|
71
|
+
// Matches the header: /** TypeName */ "def-N": <cursor here before the opening brace>
|
|
72
|
+
const extractedTypes = [];
|
|
73
|
+
const typeNames = new Set();
|
|
74
|
+
const entries = [];
|
|
75
|
+
// Maps "def-N" key → extracted type name, used to replace cross-references
|
|
76
|
+
const defToTypeName = new Map();
|
|
77
|
+
// Replace Record<...> types with object types so schema type can be extracted
|
|
78
|
+
const normalizedTypes = typeContent.replace(/(\/\*\*\s*\w+\s*\*\/\s*"def-\d+"):\s*Record<(\w+), (\w+)>;/g, `$1: { [key: $2]: $3 };`);
|
|
79
|
+
let m;
|
|
80
|
+
// Also capture the "def-N" key from the header
|
|
81
|
+
const headerPatternWithKey = /\/\*\*\s*(\w+)\s*\*\/\s*("def-\d+"):\s*(?=\{)/g;
|
|
82
|
+
while ((m = headerPatternWithKey.exec(normalizedTypes)) !== null) {
|
|
83
|
+
const typeName = m[1];
|
|
84
|
+
const defKey = m[2]; // e.g. "def-89"
|
|
85
|
+
const headerEnd = m.index + m[0].length; // points at '{'
|
|
86
|
+
// Walk forward counting braces to find the matching closing '}'
|
|
87
|
+
let depth = 0;
|
|
88
|
+
let i = headerEnd;
|
|
89
|
+
for (; i < normalizedTypes.length; i++) {
|
|
90
|
+
if (normalizedTypes[i] === '{') {
|
|
91
|
+
depth++;
|
|
92
|
+
}
|
|
93
|
+
else if (normalizedTypes[i] === '}') {
|
|
94
|
+
depth--;
|
|
95
|
+
if (depth === 0) {
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Variable i is now the index of the closing '}'
|
|
101
|
+
// The entry ends after '}' followed by ';'
|
|
102
|
+
const bodyEnd = i; // index of closing '}'
|
|
103
|
+
const entryEnd = normalizedTypes[bodyEnd + 1] === ';' ? bodyEnd + 2 : bodyEnd + 1;
|
|
104
|
+
defToTypeName.set(defKey, typeName);
|
|
105
|
+
entries.push({
|
|
106
|
+
start: m.index,
|
|
107
|
+
headerEnd,
|
|
108
|
+
typeName,
|
|
109
|
+
body: normalizedTypes.slice(headerEnd, bodyEnd + 1),
|
|
110
|
+
end: entryEnd
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
if (entries.length === 0) {
|
|
114
|
+
return normalizedTypes;
|
|
115
|
+
}
|
|
116
|
+
// Build updated content by replacing each entry's body with the type name reference
|
|
117
|
+
let updatedBaseContent = '';
|
|
118
|
+
let cursor = 0;
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
// Append everything up to the opening brace of the body
|
|
121
|
+
updatedBaseContent += normalizedTypes.slice(cursor, entry.headerEnd);
|
|
122
|
+
// Replace body with the extracted type name
|
|
123
|
+
updatedBaseContent += entry.typeName + ';';
|
|
124
|
+
cursor = entry.end;
|
|
125
|
+
// Collect the exported type (deduplicate)
|
|
126
|
+
if (!typeNames.has(entry.typeName)) {
|
|
127
|
+
typeNames.add(entry.typeName);
|
|
128
|
+
extractedTypes.push(`export type ${entry.typeName} = ${entry.body}\n`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
updatedBaseContent += normalizedTypes.slice(cursor);
|
|
132
|
+
const enumDefPrefixType = {};
|
|
133
|
+
// Replace all cross-references like components["schemas"]["def-89"] with the type name
|
|
134
|
+
// Also, extract all enum prefixes in a list
|
|
135
|
+
for (const [defKey, typeName] of defToTypeName) {
|
|
136
|
+
// The defKey already includes surrounding quotes, e.g. "def-89"
|
|
137
|
+
const refPattern = new RegExp(`components\\["schemas"]\\[${defKey}\\]`, 'g');
|
|
138
|
+
updatedBaseContent = updatedBaseContent.replace(refPattern, typeName);
|
|
139
|
+
extractedTypes.forEach((t, idx) => {
|
|
140
|
+
extractedTypes[idx] = t.replace(refPattern, typeName);
|
|
141
|
+
});
|
|
142
|
+
// Extract defKey prefix for enums to be replaced them with type name
|
|
143
|
+
const defKeyPrefix = defKey
|
|
144
|
+
.replace(/"/g, '')
|
|
145
|
+
.replace(/(^|-)([a-z])/g, (_, __, ch) => ch.toUpperCase())
|
|
146
|
+
.replace(/-(\d+)/g, '$1');
|
|
147
|
+
enumDefPrefixType[defKeyPrefix] = typeName;
|
|
148
|
+
}
|
|
149
|
+
const getFirstNumber = (value) => {
|
|
150
|
+
const match = value.match(/\d+/);
|
|
151
|
+
return match ? Number(match[0]) : -Infinity;
|
|
152
|
+
};
|
|
153
|
+
// Sort keys descending by definition ID number if present. This is required
|
|
154
|
+
// to prevent replacing the prefix 'Def125' with 'Def12' or 'Def1' sub-prefix.
|
|
155
|
+
const sortedEnumKeys = Object.keys(enumDefPrefixType).sort((a, b) => getFirstNumber(b) - getFirstNumber(a));
|
|
156
|
+
// Replace enum definition prefixes with corresponding type names
|
|
157
|
+
for (const prefix of sortedEnumKeys) {
|
|
158
|
+
const typeName = enumDefPrefixType[prefix];
|
|
159
|
+
const exportEnumPattern = new RegExp(`export enum ${prefix}(.*){`, 'g');
|
|
160
|
+
updatedBaseContent = updatedBaseContent.replace(exportEnumPattern, `export enum ${typeName}$1{`);
|
|
161
|
+
const referenceEnumPattern = new RegExp(`: ${prefix}(.*);`, 'g');
|
|
162
|
+
extractedTypes.forEach((t, idx) => {
|
|
163
|
+
extractedTypes[idx] = t.replace(referenceEnumPattern, `: ${typeName}$1;`);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
// Remove redundant "Format: ..." prose lines since the @format tag already captures this.
|
|
167
|
+
// If the line ends with */ (end-of-comment), preserve the closing; otherwise remove entirely.
|
|
168
|
+
const redundantFormatRegex = /\n(\s*)\* Format: [^\n]*/g;
|
|
169
|
+
const stripFormat = (s) => s.replace(redundantFormatRegex, (m, g) => m.endsWith('*/') ? `\n${g}*/` : '');
|
|
170
|
+
// Merge consecutive multiline comment endings and openings: */ \n /** → *
|
|
171
|
+
const doubleCommentsRegex = /(\s*)\*\/\s*\n\s*\/\*\*/g;
|
|
172
|
+
updatedBaseContent = stripFormat(updatedBaseContent.replace(doubleCommentsRegex, '$1*'));
|
|
173
|
+
const joined = extractedTypes.join('\n');
|
|
174
|
+
const mergedExtractedTypes = stripFormat(joined.replace(doubleCommentsRegex, '$1*'));
|
|
175
|
+
return updatedBaseContent + '\n' + mergedExtractedTypes;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Deduplicates and simplifies union type constituents in the provided TypeScript Abstract Syntax Tree (AST).
|
|
179
|
+
* Ensures that duplicate constituents are removed and shared types across parenthesized union branches are hoisted.
|
|
180
|
+
*
|
|
181
|
+
* @param {ts.Node[]} ast - An array of TypeScript nodes representing the AST to process and deduplicate.
|
|
182
|
+
* @return {ts.Node[]} An array of transformed TypeScript nodes with union type constituents deduplicated and simplified.
|
|
183
|
+
*/
|
|
184
|
+
function deduplicateUnionConstituents(ast) {
|
|
185
|
+
const printer = ts.createPrinter();
|
|
186
|
+
const dummyFile = ts.createSourceFile('_.ts', '', ts.ScriptTarget.Latest);
|
|
187
|
+
const printType = (node) => printer.printNode(ts.EmitHint.Unspecified, node, dummyFile);
|
|
188
|
+
const transformer = (context) => {
|
|
189
|
+
const visit = (node) => {
|
|
190
|
+
// Bottom-up: fix nested unions before outer ones
|
|
191
|
+
node = ts.visitEachChild(node, visit, context);
|
|
192
|
+
if (!ts.isUnionTypeNode(node))
|
|
193
|
+
return node;
|
|
194
|
+
const types = Array.from(node.types);
|
|
195
|
+
// Deduplicate direct top-level members
|
|
196
|
+
const seenDirect = new Set();
|
|
197
|
+
const deduped = [];
|
|
198
|
+
for (const t of types) {
|
|
199
|
+
const key = printType(t);
|
|
200
|
+
if (!seenDirect.has(key)) {
|
|
201
|
+
seenDirect.add(key);
|
|
202
|
+
deduped.push(t);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// Hoist types shared across at least 2 parenthesized union branches
|
|
206
|
+
const parenBranches = deduped
|
|
207
|
+
.map((t, i) => ({ i, t }))
|
|
208
|
+
.filter(({ t }) => ts.isParenthesizedTypeNode(t) && ts.isUnionTypeNode(t.type));
|
|
209
|
+
if (parenBranches.length < 2) {
|
|
210
|
+
// Nothing to hoist, still emit step-1 deduplication result if it changed
|
|
211
|
+
if (deduped.length === types.length)
|
|
212
|
+
return node;
|
|
213
|
+
return ts.factory.createUnionTypeNode(deduped);
|
|
214
|
+
}
|
|
215
|
+
// Count how many branches each inner type key appears in
|
|
216
|
+
const countInBranches = new Map();
|
|
217
|
+
for (const { t } of parenBranches) {
|
|
218
|
+
const innerUnion = t
|
|
219
|
+
.type;
|
|
220
|
+
const seenInBranch = new Set();
|
|
221
|
+
for (const inner of innerUnion.types) {
|
|
222
|
+
const key = printType(inner);
|
|
223
|
+
if (!seenInBranch.has(key)) {
|
|
224
|
+
seenInBranch.add(key);
|
|
225
|
+
if (countInBranches.has(key)) {
|
|
226
|
+
countInBranches.get(key).count++;
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
countInBranches.set(key, { count: 1, node: inner });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// Types that appear in at least 2 branches, or already exist directly in the outer union
|
|
235
|
+
const toHoist = new Map();
|
|
236
|
+
for (const [key, { count, node: n }] of countInBranches) {
|
|
237
|
+
if (count >= 2 || seenDirect.has(key)) {
|
|
238
|
+
toHoist.set(key, n);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (toHoist.size === 0) {
|
|
242
|
+
if (deduped.length === types.length)
|
|
243
|
+
return node;
|
|
244
|
+
return ts.factory.createUnionTypeNode(deduped);
|
|
245
|
+
}
|
|
246
|
+
// Rebuild the union
|
|
247
|
+
const result = [];
|
|
248
|
+
const hoistedKeys = new Set(toHoist.keys());
|
|
249
|
+
for (const t of deduped) {
|
|
250
|
+
if (ts.isParenthesizedTypeNode(t) && ts.isUnionTypeNode(t.type)) {
|
|
251
|
+
const innerUnion = t.type;
|
|
252
|
+
const remaining = innerUnion.types.filter((inner) => !hoistedKeys.has(printType(inner)));
|
|
253
|
+
if (remaining.length === 0) {
|
|
254
|
+
// Branch fully absorbed — discard
|
|
255
|
+
}
|
|
256
|
+
else if (remaining.length === 1) {
|
|
257
|
+
result.push(remaining[0]); // unwrap
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
result.push(ts.factory.createParenthesizedType(ts.factory.createUnionTypeNode(remaining)));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
result.push(t);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// Append each hoisted type once (skip if already present directly)
|
|
268
|
+
for (const [key, hoistedNode] of toHoist) {
|
|
269
|
+
if (!seenDirect.has(key)) {
|
|
270
|
+
result.push(hoistedNode);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return ts.factory.createUnionTypeNode(result);
|
|
274
|
+
};
|
|
275
|
+
return (root) => ts.visitNode(root, visit);
|
|
276
|
+
};
|
|
277
|
+
return ast.map((node) => ts.transform(node, [transformer]).transformed[0]);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Combines various module types into a single schema definition by analyzing the provided type content.
|
|
281
|
+
* The combined types are then appended to the original type content and used by the client to allow type
|
|
282
|
+
* inference for client modules (e.g., AuthClient<AuthModuleType>, ResourceClient<UserResourceModuleType>, etc.)
|
|
283
|
+
*
|
|
284
|
+
* @param {string} typeContent The TypeScript content containing type definitions to be processed.
|
|
285
|
+
* @param {OpenAPI3} schema The OpenAPI v3 schema object.
|
|
286
|
+
* @return {string} A string representing the modified type content with additional module type definitions.
|
|
287
|
+
*/
|
|
288
|
+
function combineModuleTypes(typeContent, schema) {
|
|
289
|
+
// Map of resource names with corresponding used types
|
|
290
|
+
// (e.g., User -> { single: UserSingle, ...})
|
|
291
|
+
const resourceTypes = {};
|
|
292
|
+
// Map of used auth types in generated schema
|
|
293
|
+
// (e.g., { loginRequest: LoginRequest })
|
|
294
|
+
const authTypes = {};
|
|
295
|
+
// Map of used auth types in generated schema
|
|
296
|
+
// (e.g., { statusResponse: AccountStatusResponse })
|
|
297
|
+
const accountTypes = {};
|
|
298
|
+
// Map of used health types in generated schema
|
|
299
|
+
// (e.g., { checkResponse: HealthCheckResponse })
|
|
300
|
+
const healthTypes = {};
|
|
301
|
+
// Utility function for capitalizing type names
|
|
302
|
+
const capitalize = (text) => text.charAt(0).toUpperCase() + text.slice(1);
|
|
303
|
+
// Utility function for collecting used types
|
|
304
|
+
const collectUsedTypes = (sourceTypes, targetObj, getTypeName, typesContent) => {
|
|
305
|
+
for (const type of sourceTypes) {
|
|
306
|
+
const typeName = getTypeName(type);
|
|
307
|
+
const resourcePattern = new RegExp(`export type ${typeName} = {`, 'g');
|
|
308
|
+
if (resourcePattern.test(typesContent)) {
|
|
309
|
+
targetObj[type] = typeName;
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
targetObj[type] = 'never';
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
// Utility function for building TypeScript type from used values
|
|
317
|
+
const buildModuleType = (name, types) => {
|
|
318
|
+
const typeValues = Object.entries(types).map(([key, value]) => `${key}: ${value}`);
|
|
319
|
+
return `export type ${name} = { ${typeValues.join(';')} };`;
|
|
320
|
+
};
|
|
321
|
+
// Find all resource types with used schema types
|
|
322
|
+
for (const type of RESOURCE_TYPES) {
|
|
323
|
+
const typeSuffix = capitalize(type);
|
|
324
|
+
const resourcePattern = new RegExp(`export type (.+)${typeSuffix} = {`, 'g');
|
|
325
|
+
let match;
|
|
326
|
+
while ((match = resourcePattern.exec(typeContent)) !== null) {
|
|
327
|
+
const resourceName = match[1];
|
|
328
|
+
resourceTypes[resourceName] ??= {};
|
|
329
|
+
resourceTypes[resourceName][type] = `${resourceName}${typeSuffix}`;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// Resource types that are not found in schema are set to 'never' type to
|
|
333
|
+
// prevent their usage by generated client
|
|
334
|
+
for (const type of RESOURCE_TYPES) {
|
|
335
|
+
for (const resourceType of Object.values(resourceTypes)) {
|
|
336
|
+
if (!resourceType[type]) {
|
|
337
|
+
resourceType[type] = 'never';
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// Find all used auth types
|
|
342
|
+
collectUsedTypes(AUTH_TYPES, authTypes, (type) => capitalize(type), typeContent);
|
|
343
|
+
// Extract identity type from authentication '/me' success response
|
|
344
|
+
const config = schema[CONFIG_FIELD];
|
|
345
|
+
authTypes['identity'] =
|
|
346
|
+
extractResponseType(typeContent, `${config?.routePrefixes?.auth ?? '/auth'}/me`, 'get') ?? 'never';
|
|
347
|
+
// Find all used account types
|
|
348
|
+
collectUsedTypes(ACCOUNT_TYPES, accountTypes, (type) => `Account${capitalize(type)}`, typeContent);
|
|
349
|
+
// Find all used health types
|
|
350
|
+
collectUsedTypes(HEALTH_TYPES, healthTypes, (type) => `Health${capitalize(type)}`, typeContent);
|
|
351
|
+
// Combine all module types into a single string and append to the type content
|
|
352
|
+
const moduleTypes = [
|
|
353
|
+
buildModuleType(AUTH_MODULE_TYPE, authTypes),
|
|
354
|
+
buildModuleType(ACCOUNT_MODULE_TYPE, accountTypes),
|
|
355
|
+
buildModuleType(HEALTH_MODULE_TYPE, healthTypes),
|
|
356
|
+
...Object.entries(resourceTypes).map(([name, types]) => buildModuleType(`${name}${RESOURCE_MODULE_TYPE}`, types))
|
|
357
|
+
];
|
|
358
|
+
return typeContent + `\n${moduleTypes.join('\n\n')}`;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Deduplicates exported type and enum names in the provided TypeScript content.
|
|
362
|
+
* Ensures no naming conflicts between types and enums with the same name and resolves duplicate type names.
|
|
363
|
+
*
|
|
364
|
+
* @param {string} content The string content of the TypeScript file to process.
|
|
365
|
+
* @return {string} A modified string with deduplicated names for exported types and enums.
|
|
366
|
+
*/
|
|
367
|
+
function deduplicateExportedTypes(content) {
|
|
368
|
+
const escapeRegExp = (s) => {
|
|
369
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
370
|
+
};
|
|
371
|
+
// Collect type names (detect duplicates) and enum names
|
|
372
|
+
const typePattern = /\bexport type (\w+)/g;
|
|
373
|
+
const enumPattern = /\bexport enum (\w+)\s*\{/g;
|
|
374
|
+
const seenTypes = new Set();
|
|
375
|
+
const duplicateTypes = new Set();
|
|
376
|
+
const enumNames = new Set();
|
|
377
|
+
let m;
|
|
378
|
+
while ((m = typePattern.exec(content)) !== null) {
|
|
379
|
+
const name = m[1];
|
|
380
|
+
if (seenTypes.has(name)) {
|
|
381
|
+
duplicateTypes.add(name);
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
seenTypes.add(name);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
while ((m = enumPattern.exec(content)) !== null) {
|
|
388
|
+
enumNames.add(m[1]);
|
|
389
|
+
}
|
|
390
|
+
// Handle type-enum collisions
|
|
391
|
+
const typeEnumCollisions = [...seenTypes].filter((n) => enumNames.has(n));
|
|
392
|
+
for (const name of typeEnumCollisions) {
|
|
393
|
+
const escaped = escapeRegExp(name);
|
|
394
|
+
const placeholder = `__DEDUP_${name}__`;
|
|
395
|
+
// Protect enum declaration from the global replacement
|
|
396
|
+
content = content.replace(new RegExp(`\\bexport enum ${escaped}\\s*\\{`, 'g'), `export enum ${placeholder} {`);
|
|
397
|
+
// Rename all type-position occurrences (declaration + references) → NameType
|
|
398
|
+
content = content.replace(new RegExp(`\\b${escaped}\\b`, 'g'), `${name}Type`);
|
|
399
|
+
// Finalize enum name → NameEnum
|
|
400
|
+
content = content.replace(new RegExp(`\\bexport enum ${escapeRegExp(placeholder)}\\s*\\{`, 'g'), `export enum ${name}Enum {`);
|
|
401
|
+
}
|
|
402
|
+
// Handle duplicate type names (rename second occurrence only)
|
|
403
|
+
for (const name of duplicateTypes) {
|
|
404
|
+
if (typeEnumCollisions.includes(name)) {
|
|
405
|
+
// already handled above
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
const escaped = escapeRegExp(name);
|
|
409
|
+
let firstSeen = false;
|
|
410
|
+
content = content.replace(new RegExp(`\\bexport type ${escaped}\\b`, 'g'), (match) => {
|
|
411
|
+
if (!firstSeen) {
|
|
412
|
+
firstSeen = true;
|
|
413
|
+
return match;
|
|
414
|
+
}
|
|
415
|
+
return `export type ${name}Type`;
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
return content;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Replaces the `string` and `string[]` types with `File` and `File[]` respectively
|
|
422
|
+
* in TypeScript type definitions related to file uploads, only if the type body
|
|
423
|
+
* includes the annotation `@format binary`.
|
|
424
|
+
*
|
|
425
|
+
* This function processes a TypeScript file content string, identifies type definitions
|
|
426
|
+
* with names matching the pattern `export type <name>FileUpload = {...}`, and updates their
|
|
427
|
+
* definitions to reflect file-like objects instead of plain strings for better type safety.
|
|
428
|
+
*
|
|
429
|
+
* @param {string} typeContent - The TypeScript file content as a string, which
|
|
430
|
+
* contains the type definitions to be inspected and replaced.
|
|
431
|
+
* @return {string} The updated TypeScript file content, with the modified type
|
|
432
|
+
* definitions that reflect `File` and `File[]` for annotated file upload types.
|
|
433
|
+
*/
|
|
434
|
+
function replaceFileUploadTypes(typeContent) {
|
|
435
|
+
const fileUploadPattern = /export type \w+FileUpload = \{/g;
|
|
436
|
+
let result = typeContent;
|
|
437
|
+
let offset = 0;
|
|
438
|
+
let match;
|
|
439
|
+
while ((match = fileUploadPattern.exec(typeContent)) !== null) {
|
|
440
|
+
const bodyStart = match.index + match[0].length - 1; // index of opening '{'
|
|
441
|
+
// Extract body using brace-depth counting
|
|
442
|
+
let depth = 0;
|
|
443
|
+
let i = bodyStart;
|
|
444
|
+
for (; i < typeContent.length; i++) {
|
|
445
|
+
if (typeContent[i] === '{')
|
|
446
|
+
depth++;
|
|
447
|
+
else if (typeContent[i] === '}') {
|
|
448
|
+
depth--;
|
|
449
|
+
if (depth === 0)
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
const body = typeContent.slice(bodyStart, i + 1);
|
|
454
|
+
// Guard: only transform if the body contains @format binary
|
|
455
|
+
if (!body.includes('@format binary')) {
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
// Replace string[] before string to avoid double-replacement
|
|
459
|
+
const newBody = body
|
|
460
|
+
.replace(/\bstring\[]/g, 'File[]')
|
|
461
|
+
.replace(/\bstring\b/g, 'File');
|
|
462
|
+
// Splice the replacement into the result, accounting for prior replacements
|
|
463
|
+
const absoluteStart = match.index + match[0].length - 1 + offset;
|
|
464
|
+
const absoluteEnd = absoluteStart + body.length;
|
|
465
|
+
result =
|
|
466
|
+
result.slice(0, absoluteStart) + newBody + result.slice(absoluteEnd);
|
|
467
|
+
offset += newBody.length - body.length;
|
|
468
|
+
}
|
|
469
|
+
return result;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Extracts the response type from a provided string of types based on the specified path, method, and status code.
|
|
473
|
+
*
|
|
474
|
+
* @param {string} types - The string containing type definitions.
|
|
475
|
+
* @param {string} path - The full API path to match in the type definitions.
|
|
476
|
+
* @param {string} method - The HTTP method to look for (e.g., get, post, put, delete).
|
|
477
|
+
* @param {string} [code=200] - The HTTP response status code to match.
|
|
478
|
+
* @param {string} [mimeType='application/json'] - The HTTP response mime type to match.
|
|
479
|
+
* @return {string | undefined} The extracted response type if a match is found, otherwise undefined.
|
|
480
|
+
*/
|
|
481
|
+
function extractResponseType(types, path, method, code = '200', mimeType = 'application/json') {
|
|
482
|
+
const escapedPath = path.replace(/[/[\]{}.*+?^$|()\\]/g, '\\$&');
|
|
483
|
+
const escapedMimeType = mimeType.replace(/[/[\]{}.*+?^$|()\\]/g, '\\$&');
|
|
484
|
+
const regex = new RegExp(`"${escapedPath}":\\s*\\{.*?${method}:\\s*\\{.*?"?${code}"?:\\s*\\{.*?"${escapedMimeType}":\\s*(\\w+)`, 's');
|
|
485
|
+
return types.match(regex)?.[1];
|
|
486
|
+
}
|
package/esm/index.d.ts
ADDED
package/esm/index.js
ADDED
package/esm/package.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './routes.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './routes.js';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type RoutePathConfig = {
|
|
2
|
+
resourcePaths: {
|
|
3
|
+
name: string;
|
|
4
|
+
basePath: string;
|
|
5
|
+
}[];
|
|
6
|
+
routePrefixes: {
|
|
7
|
+
api: string;
|
|
8
|
+
static: string;
|
|
9
|
+
health: string;
|
|
10
|
+
auth: string;
|
|
11
|
+
account: string;
|
|
12
|
+
files: string;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
export type RouteType = 'resource' | 'auth' | 'account' | 'health' | 'files' | 'custom';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './schema-util.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './schema-util.js';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fsp from 'node:fs/promises';
|
|
2
|
+
import yaml from 'js-yaml';
|
|
3
|
+
export async function readSchemaContent(schemaPath) {
|
|
4
|
+
let schemaUrl;
|
|
5
|
+
try {
|
|
6
|
+
schemaUrl = new URL(schemaPath);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return await fsp.readFile(schemaPath, 'utf8');
|
|
10
|
+
}
|
|
11
|
+
if (schemaUrl.protocol === 'http:' || schemaUrl.protocol === 'https:') {
|
|
12
|
+
let res;
|
|
13
|
+
try {
|
|
14
|
+
res = await fetch(schemaUrl.toString());
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
throw new Error(`Cannot access schema URL: ${schemaUrl}`);
|
|
18
|
+
}
|
|
19
|
+
if (!res.ok) {
|
|
20
|
+
throw new Error(`Fetching schema ${schemaUrl} failed: ${res.statusText}`);
|
|
21
|
+
}
|
|
22
|
+
return await res.text();
|
|
23
|
+
}
|
|
24
|
+
return await fsp.readFile(schemaUrl, 'utf8');
|
|
25
|
+
}
|
|
26
|
+
export async function toSchemaObject(schemaContent) {
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(schemaContent);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// not JSON, try YAML
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
return yaml.load(schemaContent);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
throw Error('Unable to parse schema object in JSON or YAML format.');
|
|
38
|
+
}
|
|
39
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appweaver/client",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.24",
|
|
4
4
|
"description": "Appweaver - the backend framework for AI-first development (@client)",
|
|
5
5
|
"author": "Luka Matosevic",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"main": "./index.js",
|
|
8
|
-
"
|
|
7
|
+
"main": "./cjs/index.js",
|
|
8
|
+
"module": "./esm/index.js",
|
|
9
|
+
"types": "./cjs/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": {
|
|
13
|
+
"types": "./esm/index.d.ts",
|
|
14
|
+
"default": "./esm/index.js"
|
|
15
|
+
},
|
|
16
|
+
"require": {
|
|
17
|
+
"types": "./cjs/index.d.ts",
|
|
18
|
+
"default": "./cjs/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"./angular": {
|
|
22
|
+
"import": {
|
|
23
|
+
"types": "./esm/angular/index.d.ts",
|
|
24
|
+
"default": "./esm/angular/index.js"
|
|
25
|
+
},
|
|
26
|
+
"require": {
|
|
27
|
+
"types": "./cjs/angular/index.d.ts",
|
|
28
|
+
"default": "./cjs/angular/index.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"./package.json": "./package.json"
|
|
32
|
+
},
|
|
9
33
|
"bin": {
|
|
10
|
-
"weaver-client": "./weaver-client.js"
|
|
34
|
+
"weaver-client": "./cjs/weaver-client.js"
|
|
11
35
|
},
|
|
12
36
|
"repository": {
|
|
13
37
|
"type": "git",
|
|
@@ -28,7 +52,7 @@
|
|
|
28
52
|
"node": ">= 20"
|
|
29
53
|
},
|
|
30
54
|
"scripts": {
|
|
31
|
-
"weaver-client": "node ./dist/weaver-client.js"
|
|
55
|
+
"weaver-client": "node ./dist/cjs/weaver-client.js"
|
|
32
56
|
},
|
|
33
57
|
"dependencies": {
|
|
34
58
|
"commander": "15.0.0",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|