@appweaver/client 1.2.0 → 1.3.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/cjs/constants.d.ts +5 -1
- package/cjs/constants.js +8 -1
- package/cjs/generators/generate-types.d.ts +1 -1
- package/cjs/generators/generate-types.js +34 -32
- package/cjs/utils/enum-util.d.ts +22 -0
- package/cjs/utils/enum-util.js +179 -0
- package/cjs/utils/index.d.ts +1 -0
- package/cjs/utils/index.js +1 -0
- package/esm/constants.d.ts +5 -1
- package/esm/constants.js +7 -0
- package/esm/generators/generate-types.d.ts +1 -1
- package/esm/generators/generate-types.js +35 -33
- package/esm/utils/enum-util.d.ts +22 -0
- package/esm/utils/enum-util.js +176 -0
- package/esm/utils/index.d.ts +1 -0
- package/esm/utils/index.js +1 -0
- package/package.json +2 -2
package/cjs/constants.d.ts
CHANGED
|
@@ -4,6 +4,10 @@ export declare const FRAMEWORKS: readonly ["fetch", "angular"];
|
|
|
4
4
|
export declare const CONFIG_FIELD = "x-appweaver-config";
|
|
5
5
|
/** Custom OpenAPI extension key used for extracting resources names from schema CRUD objects. */
|
|
6
6
|
export declare const CONFIG_RESOURCE_FIELD = "x-appweaver-resource";
|
|
7
|
+
/** Names given to the well-known enums an Appweaver schema repeats inline across its definitions.
|
|
8
|
+
* The key is the enum values joined by a `|`; the value is the name of the shared type generated
|
|
9
|
+
* for them. Enums that are not listed here are named after the definitions declaring them. */
|
|
10
|
+
export declare const SHARED_ENUM_NAMES: Record<string, string>;
|
|
7
11
|
/** Suffix used when generating the TypeScript module type name for a resource. */
|
|
8
12
|
export declare const RESOURCE_MODULE_TYPE = "ResourceModuleType";
|
|
9
13
|
/** Maps CRUD operation names to HTTP methods used for matching OpenAPI paths to `ResourceClient` methods. */
|
|
@@ -19,7 +23,7 @@ export declare const RESOURCE_OPERATIONS: {
|
|
|
19
23
|
readonly deleteFiles: "post";
|
|
20
24
|
};
|
|
21
25
|
/** Expected type keys for a resource used for type inference. */
|
|
22
|
-
export declare const RESOURCE_TYPES: readonly ["single", "multiple", "create", "update", "queryRequest", "queryResponse", "aggregateRequest", "aggregateResponse", "exportRequest", "files", "fileUpload", "fileDelete"];
|
|
26
|
+
export declare const RESOURCE_TYPES: readonly ["single", "multiple", "create", "update", "queryRequest", "queryResponse", "querySort", "aggregateRequest", "aggregateResponse", "exportRequest", "files", "fileUpload", "fileDelete"];
|
|
23
27
|
/** Type name used when generating the TypeScript module type for the auth module. */
|
|
24
28
|
export declare const AUTH_MODULE_TYPE = "AuthModuleType";
|
|
25
29
|
/** Maps auth operation names to HTTP methods used for matchingOpenAPI paths to `AuthClient` methods.
|
package/cjs/constants.js
CHANGED
|
@@ -3,13 +3,19 @@
|
|
|
3
3
|
// They are typically only modified when changes occur in other Appweaver packages
|
|
4
4
|
// (such as core and common) to reflect new route paths or methods.
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.FILE_OPERATIONS = exports.HEALTH_TYPES = exports.HEALTH_OPERATIONS = exports.HEALTH_MODULE_TYPE = exports.ACCOUNT_TYPES = exports.ACCOUNT_OPERATIONS = exports.ACCOUNT_MODULE_TYPE = exports.AUTH_TYPES = exports.AUTH_OPERATIONS = exports.AUTH_MODULE_TYPE = exports.RESOURCE_TYPES = exports.RESOURCE_OPERATIONS = exports.RESOURCE_MODULE_TYPE = exports.CONFIG_RESOURCE_FIELD = exports.CONFIG_FIELD = exports.FRAMEWORKS = void 0;
|
|
6
|
+
exports.FILE_OPERATIONS = exports.HEALTH_TYPES = exports.HEALTH_OPERATIONS = exports.HEALTH_MODULE_TYPE = exports.ACCOUNT_TYPES = exports.ACCOUNT_OPERATIONS = exports.ACCOUNT_MODULE_TYPE = exports.AUTH_TYPES = exports.AUTH_OPERATIONS = exports.AUTH_MODULE_TYPE = exports.RESOURCE_TYPES = exports.RESOURCE_OPERATIONS = exports.RESOURCE_MODULE_TYPE = exports.SHARED_ENUM_NAMES = exports.CONFIG_RESOURCE_FIELD = exports.CONFIG_FIELD = exports.FRAMEWORKS = void 0;
|
|
7
7
|
/** Supported frameworks for generating the client class. */
|
|
8
8
|
exports.FRAMEWORKS = ['fetch', 'angular'];
|
|
9
9
|
/** Custom OpenAPI extension key used for extracting route prefixes and base paths to their resources. */
|
|
10
10
|
exports.CONFIG_FIELD = 'x-appweaver-config';
|
|
11
11
|
/** Custom OpenAPI extension key used for extracting resources names from schema CRUD objects. */
|
|
12
12
|
exports.CONFIG_RESOURCE_FIELD = 'x-appweaver-resource';
|
|
13
|
+
/** Names given to the well-known enums an Appweaver schema repeats inline across its definitions.
|
|
14
|
+
* The key is the enum values joined by a `|`; the value is the name of the shared type generated
|
|
15
|
+
* for them. Enums that are not listed here are named after the definitions declaring them. */
|
|
16
|
+
exports.SHARED_ENUM_NAMES = {
|
|
17
|
+
'asc|desc': 'SortDirection'
|
|
18
|
+
};
|
|
13
19
|
/** Suffix used when generating the TypeScript module type name for a resource. */
|
|
14
20
|
exports.RESOURCE_MODULE_TYPE = 'ResourceModuleType';
|
|
15
21
|
/** Maps CRUD operation names to HTTP methods used for matching OpenAPI paths to `ResourceClient` methods. */
|
|
@@ -32,6 +38,7 @@ exports.RESOURCE_TYPES = [
|
|
|
32
38
|
'update',
|
|
33
39
|
'queryRequest',
|
|
34
40
|
'queryResponse',
|
|
41
|
+
'querySort',
|
|
35
42
|
'aggregateRequest',
|
|
36
43
|
'aggregateResponse',
|
|
37
44
|
'exportRequest',
|
|
@@ -3,7 +3,7 @@ import { OpenAPI3 } from 'openapi-typescript';
|
|
|
3
3
|
* Generates TypeScript types based on an OpenAPI V3 schema content.
|
|
4
4
|
*
|
|
5
5
|
* @param {string | OpenAPI3} schema - The OpenAPI V3 schema to generate types from. The value can be a string
|
|
6
|
-
* representing JSON or YAML format, or already parsed OpenAPI3 object.
|
|
6
|
+
* representing JSON or YAML format, or an already parsed OpenAPI3 object.
|
|
7
7
|
* @return {Promise<string>} A promise that resolves to a string containing the generated TypeScript types.
|
|
8
8
|
*/
|
|
9
9
|
export declare function generateTypes(schema: string | OpenAPI3): Promise<string>;
|
|
@@ -45,11 +45,16 @@ const constants_1 = require("../constants");
|
|
|
45
45
|
* Generates TypeScript types based on an OpenAPI V3 schema content.
|
|
46
46
|
*
|
|
47
47
|
* @param {string | OpenAPI3} schema - The OpenAPI V3 schema to generate types from. The value can be a string
|
|
48
|
-
* representing JSON or YAML format, or already parsed OpenAPI3 object.
|
|
48
|
+
* representing JSON or YAML format, or an already parsed OpenAPI3 object.
|
|
49
49
|
* @return {Promise<string>} A promise that resolves to a string containing the generated TypeScript types.
|
|
50
50
|
*/
|
|
51
51
|
async function generateTypes(schema) {
|
|
52
|
-
|
|
52
|
+
// The schema is copied before the shared enums are hoisted into it, so the
|
|
53
|
+
// caller keeps the schema it passed in unchanged
|
|
54
|
+
const schemaObject = typeof schema === 'string'
|
|
55
|
+
? await (0, utils_1.toSchemaObject)(schema)
|
|
56
|
+
: structuredClone(schema);
|
|
57
|
+
const sharedEnums = (0, utils_1.hoistSharedEnums)(schemaObject);
|
|
53
58
|
const ast = await (0, openapi_typescript_1.default)(schemaObject, {
|
|
54
59
|
exportType: true,
|
|
55
60
|
emptyObjectsUnknown: true,
|
|
@@ -86,7 +91,7 @@ async function generateTypes(schema) {
|
|
|
86
91
|
}
|
|
87
92
|
});
|
|
88
93
|
let typesContent = (0, openapi_typescript_1.astToString)(deduplicateUnionConstituents(ast));
|
|
89
|
-
typesContent = extractSchemaTypes(typesContent);
|
|
94
|
+
typesContent = extractSchemaTypes(typesContent, sharedEnums);
|
|
90
95
|
typesContent = combineModuleTypes(typesContent, schemaObject);
|
|
91
96
|
typesContent = deduplicateExportedTypes(typesContent);
|
|
92
97
|
return replaceFileUploadTypes(typesContent);
|
|
@@ -104,20 +109,26 @@ async function generateTypes(schema) {
|
|
|
104
109
|
* And replaces the inline body in `schemas` with a reference to the new type.
|
|
105
110
|
*
|
|
106
111
|
* @param {string} typeContent - The generated TypeScript type content as a string.
|
|
112
|
+
* @param {string[]} sharedEnums - The names of the definitions holding the enums shared
|
|
113
|
+
* between the other definitions, whose references are replaced by the name alone.
|
|
107
114
|
* @return {string} The transformed types content with extracted schema types.
|
|
108
115
|
*/
|
|
109
|
-
function extractSchemaTypes(typeContent) {
|
|
110
|
-
// Matches the header: /** TypeName */ "def-N": <cursor here before the opening brace>
|
|
116
|
+
function extractSchemaTypes(typeContent, sharedEnums = []) {
|
|
111
117
|
const extractedTypes = [];
|
|
112
118
|
const typeNames = new Set();
|
|
113
119
|
const entries = [];
|
|
114
120
|
// Maps "def-N" key → extracted type name, used to replace cross-references
|
|
115
121
|
const defToTypeName = new Map();
|
|
122
|
+
// Matches the JSDoc header of a definition, capturing its name, which is the first
|
|
123
|
+
// word of the comment, whether it stands alone or is followed by the description and
|
|
124
|
+
// the tags of the definition. The body of the comment is matched one character at a
|
|
125
|
+
// time, so that a header never spans the comments of the definitions before it.
|
|
126
|
+
const headerComment = String.raw `\/\*\*[\s*]*(\w+)(?:(?!\*\/)[\s\S])*\*\/`;
|
|
116
127
|
// Replace Record<...> types with object types so schema type can be extracted
|
|
117
|
-
const normalizedTypes = typeContent.replace(
|
|
128
|
+
const normalizedTypes = typeContent.replace(new RegExp(`(${headerComment}\\s*"def-\\d+"):\\s*Record<(\\w+), (\\w+)>;`, 'g'), `$1: { [key: $3]: $4 };`);
|
|
118
129
|
let m;
|
|
119
130
|
// Also capture the "def-N" key from the header
|
|
120
|
-
const headerPatternWithKey =
|
|
131
|
+
const headerPatternWithKey = new RegExp(`${headerComment}\\s*("def-\\d+"):\\s*(?=\\{)`, 'g');
|
|
121
132
|
while ((m = headerPatternWithKey.exec(normalizedTypes)) !== null) {
|
|
122
133
|
const typeName = m[1];
|
|
123
134
|
const defKey = m[2]; // e.g. "def-89"
|
|
@@ -168,40 +179,31 @@ function extractSchemaTypes(typeContent) {
|
|
|
168
179
|
}
|
|
169
180
|
}
|
|
170
181
|
updatedBaseContent += normalizedTypes.slice(cursor);
|
|
171
|
-
const enumDefPrefixType = {};
|
|
172
182
|
// Replace all cross-references like components["schemas"]["def-89"] with the type name
|
|
173
|
-
|
|
183
|
+
const references = new Map(sharedEnums.map((name) => [`"${name}"`, name]));
|
|
174
184
|
for (const [defKey, typeName] of defToTypeName) {
|
|
185
|
+
references.set(defKey, typeName);
|
|
186
|
+
}
|
|
187
|
+
for (const [defKey, typeName] of references) {
|
|
175
188
|
// The defKey already includes surrounding quotes, e.g. "def-89"
|
|
176
189
|
const refPattern = new RegExp(`components\\["schemas"]\\[${defKey}\\]`, 'g');
|
|
177
190
|
updatedBaseContent = updatedBaseContent.replace(refPattern, typeName);
|
|
178
191
|
extractedTypes.forEach((t, idx) => {
|
|
179
192
|
extractedTypes[idx] = t.replace(refPattern, typeName);
|
|
180
193
|
});
|
|
181
|
-
// Extract defKey prefix for enums to be replaced them with type name
|
|
182
|
-
const defKeyPrefix = defKey
|
|
183
|
-
.replace(/"/g, '')
|
|
184
|
-
.replace(/(^|-)([a-z])/g, (_, __, ch) => ch.toUpperCase())
|
|
185
|
-
.replace(/-(\d+)/g, '$1');
|
|
186
|
-
enumDefPrefixType[defKeyPrefix] = typeName;
|
|
187
|
-
}
|
|
188
|
-
const getFirstNumber = (value) => {
|
|
189
|
-
const match = value.match(/\d+/);
|
|
190
|
-
return match ? Number(match[0]) : -Infinity;
|
|
191
|
-
};
|
|
192
|
-
// Sort keys descending by definition ID number if present. This is required
|
|
193
|
-
// to prevent replacing the prefix 'Def125' with 'Def12' or 'Def1' sub-prefix.
|
|
194
|
-
const sortedEnumKeys = Object.keys(enumDefPrefixType).sort((a, b) => getFirstNumber(b) - getFirstNumber(a));
|
|
195
|
-
// Replace enum definition prefixes with corresponding type names
|
|
196
|
-
for (const prefix of sortedEnumKeys) {
|
|
197
|
-
const typeName = enumDefPrefixType[prefix];
|
|
198
|
-
const exportEnumPattern = new RegExp(`export enum ${prefix}(.*){`, 'g');
|
|
199
|
-
updatedBaseContent = updatedBaseContent.replace(exportEnumPattern, `export enum ${typeName}$1{`);
|
|
200
|
-
const referenceEnumPattern = new RegExp(`: ${prefix}(.*);`, 'g');
|
|
201
|
-
extractedTypes.forEach((t, idx) => {
|
|
202
|
-
extractedTypes[idx] = t.replace(referenceEnumPattern, `: ${typeName}$1;`);
|
|
203
|
-
});
|
|
204
194
|
}
|
|
195
|
+
// Rename the enums generated for the properties of a definition (i.e. `Def89Status`)
|
|
196
|
+
// after the type extracted from it (i.e. `PostSingleStatus`), declarations and
|
|
197
|
+
// references alike. The digits are matched as a whole, so a definition number is never
|
|
198
|
+
// mistaken for the prefix of a longer one (i.e. `Def12` inside of `Def125Status`).
|
|
199
|
+
const renameEnums = (content) => content.replace(/\bDef(\d+)([A-Z]\w*)\b/g, (match, id, property) => {
|
|
200
|
+
const typeName = defToTypeName.get(`"def-${id}"`);
|
|
201
|
+
return typeName ? `${typeName}${property}` : match;
|
|
202
|
+
});
|
|
203
|
+
updatedBaseContent = renameEnums(updatedBaseContent);
|
|
204
|
+
extractedTypes.forEach((t, idx) => {
|
|
205
|
+
extractedTypes[idx] = renameEnums(t);
|
|
206
|
+
});
|
|
205
207
|
// Remove redundant "Format: ..." prose lines since the @format tag already captures this.
|
|
206
208
|
// If the line ends with */ (end-of-comment), preserve the closing; otherwise remove entirely.
|
|
207
209
|
const redundantFormatRegex = /\n(\s*)\* Format: [^\n]*/g;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { OpenAPI3 } from 'openapi-typescript';
|
|
2
|
+
/**
|
|
3
|
+
* Hoists the inline enums a schema repeats across its definitions into shared definitions
|
|
4
|
+
* of their own, replacing every occurrence with a reference to the hoisted definition.
|
|
5
|
+
*
|
|
6
|
+
* An Appweaver schema declares the same enum inline over and over, once per property that
|
|
7
|
+
* accepts it, and each of them would otherwise become an enum of its own in the generated
|
|
8
|
+
* types (i.e. a separate `asc | desc` enum for every sortable field of every resource).
|
|
9
|
+
*
|
|
10
|
+
* Only enums that are byte for byte identical are hoisted, so no property loses its
|
|
11
|
+
* description, example or nullability along the way. The hoisted definition is named after
|
|
12
|
+
* {@link SHARED_ENUM_NAMES} when its values are well known, and otherwise after the part
|
|
13
|
+
* the declaring definitions have in common followed by the property name (i.e. `PostCreate`
|
|
14
|
+
* and `PostSingle` declaring `status` give `PostStatus`). Enums whose name cannot be
|
|
15
|
+
* resolved, or whose name is already taken, are left inline.
|
|
16
|
+
*
|
|
17
|
+
* The given schema is mutated in place.
|
|
18
|
+
*
|
|
19
|
+
* @param {OpenAPI3} schema The OpenAPI v3 schema to hoist the shared enums of.
|
|
20
|
+
* @return {string[]} The names of the hoisted definitions, in the order they were created.
|
|
21
|
+
*/
|
|
22
|
+
export declare function hoistSharedEnums(schema: OpenAPI3): string[];
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hoistSharedEnums = hoistSharedEnums;
|
|
4
|
+
const constants_1 = require("../constants");
|
|
5
|
+
/**
|
|
6
|
+
* Hoists the inline enums a schema repeats across its definitions into shared definitions
|
|
7
|
+
* of their own, replacing every occurrence with a reference to the hoisted definition.
|
|
8
|
+
*
|
|
9
|
+
* An Appweaver schema declares the same enum inline over and over, once per property that
|
|
10
|
+
* accepts it, and each of them would otherwise become an enum of its own in the generated
|
|
11
|
+
* types (i.e. a separate `asc | desc` enum for every sortable field of every resource).
|
|
12
|
+
*
|
|
13
|
+
* Only enums that are byte for byte identical are hoisted, so no property loses its
|
|
14
|
+
* description, example or nullability along the way. The hoisted definition is named after
|
|
15
|
+
* {@link SHARED_ENUM_NAMES} when its values are well known, and otherwise after the part
|
|
16
|
+
* the declaring definitions have in common followed by the property name (i.e. `PostCreate`
|
|
17
|
+
* and `PostSingle` declaring `status` give `PostStatus`). Enums whose name cannot be
|
|
18
|
+
* resolved, or whose name is already taken, are left inline.
|
|
19
|
+
*
|
|
20
|
+
* The given schema is mutated in place.
|
|
21
|
+
*
|
|
22
|
+
* @param {OpenAPI3} schema The OpenAPI v3 schema to hoist the shared enums of.
|
|
23
|
+
* @return {string[]} The names of the hoisted definitions, in the order they were created.
|
|
24
|
+
*/
|
|
25
|
+
function hoistSharedEnums(schema) {
|
|
26
|
+
const definitions = schema.components?.schemas;
|
|
27
|
+
if (!definitions) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
// Enums declared by identical schema content, keyed by that content
|
|
31
|
+
const groups = new Map();
|
|
32
|
+
for (const [key, definition] of Object.entries(definitions)) {
|
|
33
|
+
if (definition && typeof definition === 'object') {
|
|
34
|
+
const owner = definition['title'];
|
|
35
|
+
collectEnums(definition, typeof owner === 'string' ? owner : key, '', groups);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const takenNames = new Set(Object.keys(definitions));
|
|
39
|
+
for (const definition of Object.values(definitions)) {
|
|
40
|
+
const title = definition?.['title'];
|
|
41
|
+
if (typeof title === 'string') {
|
|
42
|
+
takenNames.add(title);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const hoistedNames = [];
|
|
46
|
+
for (const [content, occurrences] of groups) {
|
|
47
|
+
if (occurrences.length < 2) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const name = resolveEnumName(JSON.parse(content), occurrences);
|
|
51
|
+
if (!name || takenNames.has(name)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
takenNames.add(name);
|
|
55
|
+
hoistedNames.push(name);
|
|
56
|
+
definitions[name] = { title: name, ...JSON.parse(content) };
|
|
57
|
+
for (const { container, key } of occurrences) {
|
|
58
|
+
container[key] = {
|
|
59
|
+
$ref: `#/components/schemas/${name}`
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return hoistedNames;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Collects every inline enum declared below the given schema node into the groups map,
|
|
67
|
+
* keyed by the stable serialization of the enum schema, so identical enums group together.
|
|
68
|
+
*
|
|
69
|
+
* @param {SchemaContainer} container The schema node to collect the enums of.
|
|
70
|
+
* @param {string} owner The name of the definition the node belongs to.
|
|
71
|
+
* @param {string} property The name of the property the node is declared under, if any.
|
|
72
|
+
* @param {Map<string, EnumOccurrence[]>} groups The collected occurrences, grouped by content.
|
|
73
|
+
*/
|
|
74
|
+
function collectEnums(container, owner, property, groups) {
|
|
75
|
+
const entries = Array.isArray(container)
|
|
76
|
+
? container.map((value, index) => [index, value])
|
|
77
|
+
: Object.entries(container);
|
|
78
|
+
for (const [key, value] of entries) {
|
|
79
|
+
if (!value || typeof value !== 'object') {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
// The keys of a `properties` object name the schemas below them, everything
|
|
83
|
+
// else keeps the property name of the node it was reached through
|
|
84
|
+
if (key === 'properties' && !Array.isArray(value)) {
|
|
85
|
+
const properties = value;
|
|
86
|
+
for (const name of Object.keys(properties)) {
|
|
87
|
+
visitSchema(properties, name, owner, name, groups);
|
|
88
|
+
}
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
visitSchema(container, key, owner, property, groups);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Records the schema held by the container under the given key as an enum occurrence, or
|
|
96
|
+
* descends into it when it declares no enum of its own.
|
|
97
|
+
*
|
|
98
|
+
* @param {SchemaContainer} container The container holding the schema node.
|
|
99
|
+
* @param {string | number} key The key the schema node is held under.
|
|
100
|
+
* @param {string} owner The name of the definition the node belongs to.
|
|
101
|
+
* @param {string} property The name of the property the node is declared under, if any.
|
|
102
|
+
* @param {Map<string, EnumOccurrence[]>} groups The collected occurrences, grouped by content.
|
|
103
|
+
*/
|
|
104
|
+
function visitSchema(container, key, owner, property, groups) {
|
|
105
|
+
const node = container[key];
|
|
106
|
+
if (Array.isArray(node['enum'])) {
|
|
107
|
+
const content = stableStringify(node);
|
|
108
|
+
const occurrences = groups.get(content) ?? [];
|
|
109
|
+
occurrences.push({ container, key, owner, property });
|
|
110
|
+
groups.set(content, occurrences);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
collectEnums(node, owner, property, groups);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Resolves the name of the definition an enum is hoisted into, preferring the name given to
|
|
117
|
+
* its values by {@link SHARED_ENUM_NAMES} and falling back to the common part of the names of
|
|
118
|
+
* the definitions declaring it, followed by the property name they all declare it under.
|
|
119
|
+
*
|
|
120
|
+
* @param {Record<string, unknown>} node The enum schema to resolve the name of.
|
|
121
|
+
* @param {EnumOccurrence[]} occurrences The places the enum is declared in.
|
|
122
|
+
* @return {string | undefined} The resolved name, or undefined when the enum has no name to
|
|
123
|
+
* be hoisted under.
|
|
124
|
+
*/
|
|
125
|
+
function resolveEnumName(node, occurrences) {
|
|
126
|
+
const values = node['enum'];
|
|
127
|
+
const knownName = constants_1.SHARED_ENUM_NAMES[values.join('|')];
|
|
128
|
+
if (knownName) {
|
|
129
|
+
return knownName;
|
|
130
|
+
}
|
|
131
|
+
const { property } = occurrences[0];
|
|
132
|
+
if (!property || occurrences.some((o) => o.property !== property)) {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
const prefix = commonNamePrefix(occurrences.map((o) => o.owner));
|
|
136
|
+
if (!prefix) {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
return prefix + property.charAt(0).toUpperCase() + property.slice(1);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Resolves the longest prefix the given names share, cut at a word boundary so the result
|
|
143
|
+
* stays a readable name (i.e. `HealthCheckResponse` and `HealthCheckResult` give
|
|
144
|
+
* `HealthCheck` rather than `HealthCheckRes`).
|
|
145
|
+
*
|
|
146
|
+
* @param {string[]} names The names to find the common prefix of.
|
|
147
|
+
* @return {string} The common prefix, empty when the names start with different words.
|
|
148
|
+
*/
|
|
149
|
+
function commonNamePrefix(names) {
|
|
150
|
+
const wordsPerName = names.map((name) => name.match(/[A-Z]+(?![a-z])|[A-Z]?[a-z0-9]+|[A-Z]/g) ?? []);
|
|
151
|
+
const words = [];
|
|
152
|
+
for (let i = 0; i < wordsPerName[0].length; i++) {
|
|
153
|
+
const word = wordsPerName[0][i];
|
|
154
|
+
if (wordsPerName.some((other) => other[i] !== word)) {
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
words.push(word);
|
|
158
|
+
}
|
|
159
|
+
return words.join('');
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Serializes a value with its object keys sorted, so two schemas holding the same content
|
|
163
|
+
* in a different key order serialize alike.
|
|
164
|
+
*
|
|
165
|
+
* @param {unknown} value The value to serialize.
|
|
166
|
+
* @return {string} The stable JSON serialization of the value.
|
|
167
|
+
*/
|
|
168
|
+
function stableStringify(value) {
|
|
169
|
+
if (Array.isArray(value)) {
|
|
170
|
+
return `[${value.map(stableStringify).join(',')}]`;
|
|
171
|
+
}
|
|
172
|
+
if (value && typeof value === 'object') {
|
|
173
|
+
const entries = Object.keys(value)
|
|
174
|
+
.sort()
|
|
175
|
+
.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`);
|
|
176
|
+
return `{${entries.join(',')}}`;
|
|
177
|
+
}
|
|
178
|
+
return JSON.stringify(value) ?? 'null';
|
|
179
|
+
}
|
package/cjs/utils/index.d.ts
CHANGED
package/cjs/utils/index.js
CHANGED
|
@@ -14,4 +14,5 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./enum-util"), exports);
|
|
17
18
|
__exportStar(require("./schema-util"), exports);
|
package/esm/constants.d.ts
CHANGED
|
@@ -4,6 +4,10 @@ export declare const FRAMEWORKS: readonly ["fetch", "angular"];
|
|
|
4
4
|
export declare const CONFIG_FIELD = "x-appweaver-config";
|
|
5
5
|
/** Custom OpenAPI extension key used for extracting resources names from schema CRUD objects. */
|
|
6
6
|
export declare const CONFIG_RESOURCE_FIELD = "x-appweaver-resource";
|
|
7
|
+
/** Names given to the well-known enums an Appweaver schema repeats inline across its definitions.
|
|
8
|
+
* The key is the enum values joined by a `|`; the value is the name of the shared type generated
|
|
9
|
+
* for them. Enums that are not listed here are named after the definitions declaring them. */
|
|
10
|
+
export declare const SHARED_ENUM_NAMES: Record<string, string>;
|
|
7
11
|
/** Suffix used when generating the TypeScript module type name for a resource. */
|
|
8
12
|
export declare const RESOURCE_MODULE_TYPE = "ResourceModuleType";
|
|
9
13
|
/** Maps CRUD operation names to HTTP methods used for matching OpenAPI paths to `ResourceClient` methods. */
|
|
@@ -19,7 +23,7 @@ export declare const RESOURCE_OPERATIONS: {
|
|
|
19
23
|
readonly deleteFiles: "post";
|
|
20
24
|
};
|
|
21
25
|
/** Expected type keys for a resource used for type inference. */
|
|
22
|
-
export declare const RESOURCE_TYPES: readonly ["single", "multiple", "create", "update", "queryRequest", "queryResponse", "aggregateRequest", "aggregateResponse", "exportRequest", "files", "fileUpload", "fileDelete"];
|
|
26
|
+
export declare const RESOURCE_TYPES: readonly ["single", "multiple", "create", "update", "queryRequest", "queryResponse", "querySort", "aggregateRequest", "aggregateResponse", "exportRequest", "files", "fileUpload", "fileDelete"];
|
|
23
27
|
/** Type name used when generating the TypeScript module type for the auth module. */
|
|
24
28
|
export declare const AUTH_MODULE_TYPE = "AuthModuleType";
|
|
25
29
|
/** Maps auth operation names to HTTP methods used for matchingOpenAPI paths to `AuthClient` methods.
|
package/esm/constants.js
CHANGED
|
@@ -7,6 +7,12 @@ export const FRAMEWORKS = ['fetch', 'angular'];
|
|
|
7
7
|
export const CONFIG_FIELD = 'x-appweaver-config';
|
|
8
8
|
/** Custom OpenAPI extension key used for extracting resources names from schema CRUD objects. */
|
|
9
9
|
export const CONFIG_RESOURCE_FIELD = 'x-appweaver-resource';
|
|
10
|
+
/** Names given to the well-known enums an Appweaver schema repeats inline across its definitions.
|
|
11
|
+
* The key is the enum values joined by a `|`; the value is the name of the shared type generated
|
|
12
|
+
* for them. Enums that are not listed here are named after the definitions declaring them. */
|
|
13
|
+
export const SHARED_ENUM_NAMES = {
|
|
14
|
+
'asc|desc': 'SortDirection'
|
|
15
|
+
};
|
|
10
16
|
/** Suffix used when generating the TypeScript module type name for a resource. */
|
|
11
17
|
export const RESOURCE_MODULE_TYPE = 'ResourceModuleType';
|
|
12
18
|
/** Maps CRUD operation names to HTTP methods used for matching OpenAPI paths to `ResourceClient` methods. */
|
|
@@ -29,6 +35,7 @@ export const RESOURCE_TYPES = [
|
|
|
29
35
|
'update',
|
|
30
36
|
'queryRequest',
|
|
31
37
|
'queryResponse',
|
|
38
|
+
'querySort',
|
|
32
39
|
'aggregateRequest',
|
|
33
40
|
'aggregateResponse',
|
|
34
41
|
'exportRequest',
|
|
@@ -3,7 +3,7 @@ import { OpenAPI3 } from 'openapi-typescript';
|
|
|
3
3
|
* Generates TypeScript types based on an OpenAPI V3 schema content.
|
|
4
4
|
*
|
|
5
5
|
* @param {string | OpenAPI3} schema - The OpenAPI V3 schema to generate types from. The value can be a string
|
|
6
|
-
* representing JSON or YAML format, or already parsed OpenAPI3 object.
|
|
6
|
+
* representing JSON or YAML format, or an already parsed OpenAPI3 object.
|
|
7
7
|
* @return {Promise<string>} A promise that resolves to a string containing the generated TypeScript types.
|
|
8
8
|
*/
|
|
9
9
|
export declare function generateTypes(schema: string | OpenAPI3): Promise<string>;
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import openapiTS, { astToString } from 'openapi-typescript';
|
|
2
2
|
import ts from 'typescript';
|
|
3
|
-
import { toSchemaObject } from '../utils/index.js';
|
|
3
|
+
import { hoistSharedEnums, toSchemaObject } from '../utils/index.js';
|
|
4
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
5
|
/**
|
|
6
6
|
* Generates TypeScript types based on an OpenAPI V3 schema content.
|
|
7
7
|
*
|
|
8
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.
|
|
9
|
+
* representing JSON or YAML format, or an already parsed OpenAPI3 object.
|
|
10
10
|
* @return {Promise<string>} A promise that resolves to a string containing the generated TypeScript types.
|
|
11
11
|
*/
|
|
12
12
|
export async function generateTypes(schema) {
|
|
13
|
-
|
|
13
|
+
// The schema is copied before the shared enums are hoisted into it, so the
|
|
14
|
+
// caller keeps the schema it passed in unchanged
|
|
15
|
+
const schemaObject = typeof schema === 'string'
|
|
16
|
+
? await toSchemaObject(schema)
|
|
17
|
+
: structuredClone(schema);
|
|
18
|
+
const sharedEnums = hoistSharedEnums(schemaObject);
|
|
14
19
|
const ast = await openapiTS(schemaObject, {
|
|
15
20
|
exportType: true,
|
|
16
21
|
emptyObjectsUnknown: true,
|
|
@@ -47,7 +52,7 @@ export async function generateTypes(schema) {
|
|
|
47
52
|
}
|
|
48
53
|
});
|
|
49
54
|
let typesContent = astToString(deduplicateUnionConstituents(ast));
|
|
50
|
-
typesContent = extractSchemaTypes(typesContent);
|
|
55
|
+
typesContent = extractSchemaTypes(typesContent, sharedEnums);
|
|
51
56
|
typesContent = combineModuleTypes(typesContent, schemaObject);
|
|
52
57
|
typesContent = deduplicateExportedTypes(typesContent);
|
|
53
58
|
return replaceFileUploadTypes(typesContent);
|
|
@@ -65,20 +70,26 @@ export async function generateTypes(schema) {
|
|
|
65
70
|
* And replaces the inline body in `schemas` with a reference to the new type.
|
|
66
71
|
*
|
|
67
72
|
* @param {string} typeContent - The generated TypeScript type content as a string.
|
|
73
|
+
* @param {string[]} sharedEnums - The names of the definitions holding the enums shared
|
|
74
|
+
* between the other definitions, whose references are replaced by the name alone.
|
|
68
75
|
* @return {string} The transformed types content with extracted schema types.
|
|
69
76
|
*/
|
|
70
|
-
function extractSchemaTypes(typeContent) {
|
|
71
|
-
// Matches the header: /** TypeName */ "def-N": <cursor here before the opening brace>
|
|
77
|
+
function extractSchemaTypes(typeContent, sharedEnums = []) {
|
|
72
78
|
const extractedTypes = [];
|
|
73
79
|
const typeNames = new Set();
|
|
74
80
|
const entries = [];
|
|
75
81
|
// Maps "def-N" key → extracted type name, used to replace cross-references
|
|
76
82
|
const defToTypeName = new Map();
|
|
83
|
+
// Matches the JSDoc header of a definition, capturing its name, which is the first
|
|
84
|
+
// word of the comment, whether it stands alone or is followed by the description and
|
|
85
|
+
// the tags of the definition. The body of the comment is matched one character at a
|
|
86
|
+
// time, so that a header never spans the comments of the definitions before it.
|
|
87
|
+
const headerComment = String.raw `\/\*\*[\s*]*(\w+)(?:(?!\*\/)[\s\S])*\*\/`;
|
|
77
88
|
// Replace Record<...> types with object types so schema type can be extracted
|
|
78
|
-
const normalizedTypes = typeContent.replace(
|
|
89
|
+
const normalizedTypes = typeContent.replace(new RegExp(`(${headerComment}\\s*"def-\\d+"):\\s*Record<(\\w+), (\\w+)>;`, 'g'), `$1: { [key: $3]: $4 };`);
|
|
79
90
|
let m;
|
|
80
91
|
// Also capture the "def-N" key from the header
|
|
81
|
-
const headerPatternWithKey =
|
|
92
|
+
const headerPatternWithKey = new RegExp(`${headerComment}\\s*("def-\\d+"):\\s*(?=\\{)`, 'g');
|
|
82
93
|
while ((m = headerPatternWithKey.exec(normalizedTypes)) !== null) {
|
|
83
94
|
const typeName = m[1];
|
|
84
95
|
const defKey = m[2]; // e.g. "def-89"
|
|
@@ -129,40 +140,31 @@ function extractSchemaTypes(typeContent) {
|
|
|
129
140
|
}
|
|
130
141
|
}
|
|
131
142
|
updatedBaseContent += normalizedTypes.slice(cursor);
|
|
132
|
-
const enumDefPrefixType = {};
|
|
133
143
|
// Replace all cross-references like components["schemas"]["def-89"] with the type name
|
|
134
|
-
|
|
144
|
+
const references = new Map(sharedEnums.map((name) => [`"${name}"`, name]));
|
|
135
145
|
for (const [defKey, typeName] of defToTypeName) {
|
|
146
|
+
references.set(defKey, typeName);
|
|
147
|
+
}
|
|
148
|
+
for (const [defKey, typeName] of references) {
|
|
136
149
|
// The defKey already includes surrounding quotes, e.g. "def-89"
|
|
137
150
|
const refPattern = new RegExp(`components\\["schemas"]\\[${defKey}\\]`, 'g');
|
|
138
151
|
updatedBaseContent = updatedBaseContent.replace(refPattern, typeName);
|
|
139
152
|
extractedTypes.forEach((t, idx) => {
|
|
140
153
|
extractedTypes[idx] = t.replace(refPattern, typeName);
|
|
141
154
|
});
|
|
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
155
|
}
|
|
156
|
+
// Rename the enums generated for the properties of a definition (i.e. `Def89Status`)
|
|
157
|
+
// after the type extracted from it (i.e. `PostSingleStatus`), declarations and
|
|
158
|
+
// references alike. The digits are matched as a whole, so a definition number is never
|
|
159
|
+
// mistaken for the prefix of a longer one (i.e. `Def12` inside of `Def125Status`).
|
|
160
|
+
const renameEnums = (content) => content.replace(/\bDef(\d+)([A-Z]\w*)\b/g, (match, id, property) => {
|
|
161
|
+
const typeName = defToTypeName.get(`"def-${id}"`);
|
|
162
|
+
return typeName ? `${typeName}${property}` : match;
|
|
163
|
+
});
|
|
164
|
+
updatedBaseContent = renameEnums(updatedBaseContent);
|
|
165
|
+
extractedTypes.forEach((t, idx) => {
|
|
166
|
+
extractedTypes[idx] = renameEnums(t);
|
|
167
|
+
});
|
|
166
168
|
// Remove redundant "Format: ..." prose lines since the @format tag already captures this.
|
|
167
169
|
// If the line ends with */ (end-of-comment), preserve the closing; otherwise remove entirely.
|
|
168
170
|
const redundantFormatRegex = /\n(\s*)\* Format: [^\n]*/g;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { OpenAPI3 } from 'openapi-typescript';
|
|
2
|
+
/**
|
|
3
|
+
* Hoists the inline enums a schema repeats across its definitions into shared definitions
|
|
4
|
+
* of their own, replacing every occurrence with a reference to the hoisted definition.
|
|
5
|
+
*
|
|
6
|
+
* An Appweaver schema declares the same enum inline over and over, once per property that
|
|
7
|
+
* accepts it, and each of them would otherwise become an enum of its own in the generated
|
|
8
|
+
* types (i.e. a separate `asc | desc` enum for every sortable field of every resource).
|
|
9
|
+
*
|
|
10
|
+
* Only enums that are byte for byte identical are hoisted, so no property loses its
|
|
11
|
+
* description, example or nullability along the way. The hoisted definition is named after
|
|
12
|
+
* {@link SHARED_ENUM_NAMES} when its values are well known, and otherwise after the part
|
|
13
|
+
* the declaring definitions have in common followed by the property name (i.e. `PostCreate`
|
|
14
|
+
* and `PostSingle` declaring `status` give `PostStatus`). Enums whose name cannot be
|
|
15
|
+
* resolved, or whose name is already taken, are left inline.
|
|
16
|
+
*
|
|
17
|
+
* The given schema is mutated in place.
|
|
18
|
+
*
|
|
19
|
+
* @param {OpenAPI3} schema The OpenAPI v3 schema to hoist the shared enums of.
|
|
20
|
+
* @return {string[]} The names of the hoisted definitions, in the order they were created.
|
|
21
|
+
*/
|
|
22
|
+
export declare function hoistSharedEnums(schema: OpenAPI3): string[];
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { SHARED_ENUM_NAMES } from '../constants.js';
|
|
2
|
+
/**
|
|
3
|
+
* Hoists the inline enums a schema repeats across its definitions into shared definitions
|
|
4
|
+
* of their own, replacing every occurrence with a reference to the hoisted definition.
|
|
5
|
+
*
|
|
6
|
+
* An Appweaver schema declares the same enum inline over and over, once per property that
|
|
7
|
+
* accepts it, and each of them would otherwise become an enum of its own in the generated
|
|
8
|
+
* types (i.e. a separate `asc | desc` enum for every sortable field of every resource).
|
|
9
|
+
*
|
|
10
|
+
* Only enums that are byte for byte identical are hoisted, so no property loses its
|
|
11
|
+
* description, example or nullability along the way. The hoisted definition is named after
|
|
12
|
+
* {@link SHARED_ENUM_NAMES} when its values are well known, and otherwise after the part
|
|
13
|
+
* the declaring definitions have in common followed by the property name (i.e. `PostCreate`
|
|
14
|
+
* and `PostSingle` declaring `status` give `PostStatus`). Enums whose name cannot be
|
|
15
|
+
* resolved, or whose name is already taken, are left inline.
|
|
16
|
+
*
|
|
17
|
+
* The given schema is mutated in place.
|
|
18
|
+
*
|
|
19
|
+
* @param {OpenAPI3} schema The OpenAPI v3 schema to hoist the shared enums of.
|
|
20
|
+
* @return {string[]} The names of the hoisted definitions, in the order they were created.
|
|
21
|
+
*/
|
|
22
|
+
export function hoistSharedEnums(schema) {
|
|
23
|
+
const definitions = schema.components?.schemas;
|
|
24
|
+
if (!definitions) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
// Enums declared by identical schema content, keyed by that content
|
|
28
|
+
const groups = new Map();
|
|
29
|
+
for (const [key, definition] of Object.entries(definitions)) {
|
|
30
|
+
if (definition && typeof definition === 'object') {
|
|
31
|
+
const owner = definition['title'];
|
|
32
|
+
collectEnums(definition, typeof owner === 'string' ? owner : key, '', groups);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const takenNames = new Set(Object.keys(definitions));
|
|
36
|
+
for (const definition of Object.values(definitions)) {
|
|
37
|
+
const title = definition?.['title'];
|
|
38
|
+
if (typeof title === 'string') {
|
|
39
|
+
takenNames.add(title);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const hoistedNames = [];
|
|
43
|
+
for (const [content, occurrences] of groups) {
|
|
44
|
+
if (occurrences.length < 2) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const name = resolveEnumName(JSON.parse(content), occurrences);
|
|
48
|
+
if (!name || takenNames.has(name)) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
takenNames.add(name);
|
|
52
|
+
hoistedNames.push(name);
|
|
53
|
+
definitions[name] = { title: name, ...JSON.parse(content) };
|
|
54
|
+
for (const { container, key } of occurrences) {
|
|
55
|
+
container[key] = {
|
|
56
|
+
$ref: `#/components/schemas/${name}`
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return hoistedNames;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Collects every inline enum declared below the given schema node into the groups map,
|
|
64
|
+
* keyed by the stable serialization of the enum schema, so identical enums group together.
|
|
65
|
+
*
|
|
66
|
+
* @param {SchemaContainer} container The schema node to collect the enums of.
|
|
67
|
+
* @param {string} owner The name of the definition the node belongs to.
|
|
68
|
+
* @param {string} property The name of the property the node is declared under, if any.
|
|
69
|
+
* @param {Map<string, EnumOccurrence[]>} groups The collected occurrences, grouped by content.
|
|
70
|
+
*/
|
|
71
|
+
function collectEnums(container, owner, property, groups) {
|
|
72
|
+
const entries = Array.isArray(container)
|
|
73
|
+
? container.map((value, index) => [index, value])
|
|
74
|
+
: Object.entries(container);
|
|
75
|
+
for (const [key, value] of entries) {
|
|
76
|
+
if (!value || typeof value !== 'object') {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
// The keys of a `properties` object name the schemas below them, everything
|
|
80
|
+
// else keeps the property name of the node it was reached through
|
|
81
|
+
if (key === 'properties' && !Array.isArray(value)) {
|
|
82
|
+
const properties = value;
|
|
83
|
+
for (const name of Object.keys(properties)) {
|
|
84
|
+
visitSchema(properties, name, owner, name, groups);
|
|
85
|
+
}
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
visitSchema(container, key, owner, property, groups);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Records the schema held by the container under the given key as an enum occurrence, or
|
|
93
|
+
* descends into it when it declares no enum of its own.
|
|
94
|
+
*
|
|
95
|
+
* @param {SchemaContainer} container The container holding the schema node.
|
|
96
|
+
* @param {string | number} key The key the schema node is held under.
|
|
97
|
+
* @param {string} owner The name of the definition the node belongs to.
|
|
98
|
+
* @param {string} property The name of the property the node is declared under, if any.
|
|
99
|
+
* @param {Map<string, EnumOccurrence[]>} groups The collected occurrences, grouped by content.
|
|
100
|
+
*/
|
|
101
|
+
function visitSchema(container, key, owner, property, groups) {
|
|
102
|
+
const node = container[key];
|
|
103
|
+
if (Array.isArray(node['enum'])) {
|
|
104
|
+
const content = stableStringify(node);
|
|
105
|
+
const occurrences = groups.get(content) ?? [];
|
|
106
|
+
occurrences.push({ container, key, owner, property });
|
|
107
|
+
groups.set(content, occurrences);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
collectEnums(node, owner, property, groups);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Resolves the name of the definition an enum is hoisted into, preferring the name given to
|
|
114
|
+
* its values by {@link SHARED_ENUM_NAMES} and falling back to the common part of the names of
|
|
115
|
+
* the definitions declaring it, followed by the property name they all declare it under.
|
|
116
|
+
*
|
|
117
|
+
* @param {Record<string, unknown>} node The enum schema to resolve the name of.
|
|
118
|
+
* @param {EnumOccurrence[]} occurrences The places the enum is declared in.
|
|
119
|
+
* @return {string | undefined} The resolved name, or undefined when the enum has no name to
|
|
120
|
+
* be hoisted under.
|
|
121
|
+
*/
|
|
122
|
+
function resolveEnumName(node, occurrences) {
|
|
123
|
+
const values = node['enum'];
|
|
124
|
+
const knownName = SHARED_ENUM_NAMES[values.join('|')];
|
|
125
|
+
if (knownName) {
|
|
126
|
+
return knownName;
|
|
127
|
+
}
|
|
128
|
+
const { property } = occurrences[0];
|
|
129
|
+
if (!property || occurrences.some((o) => o.property !== property)) {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
const prefix = commonNamePrefix(occurrences.map((o) => o.owner));
|
|
133
|
+
if (!prefix) {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
return prefix + property.charAt(0).toUpperCase() + property.slice(1);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Resolves the longest prefix the given names share, cut at a word boundary so the result
|
|
140
|
+
* stays a readable name (i.e. `HealthCheckResponse` and `HealthCheckResult` give
|
|
141
|
+
* `HealthCheck` rather than `HealthCheckRes`).
|
|
142
|
+
*
|
|
143
|
+
* @param {string[]} names The names to find the common prefix of.
|
|
144
|
+
* @return {string} The common prefix, empty when the names start with different words.
|
|
145
|
+
*/
|
|
146
|
+
function commonNamePrefix(names) {
|
|
147
|
+
const wordsPerName = names.map((name) => name.match(/[A-Z]+(?![a-z])|[A-Z]?[a-z0-9]+|[A-Z]/g) ?? []);
|
|
148
|
+
const words = [];
|
|
149
|
+
for (let i = 0; i < wordsPerName[0].length; i++) {
|
|
150
|
+
const word = wordsPerName[0][i];
|
|
151
|
+
if (wordsPerName.some((other) => other[i] !== word)) {
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
words.push(word);
|
|
155
|
+
}
|
|
156
|
+
return words.join('');
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Serializes a value with its object keys sorted, so two schemas holding the same content
|
|
160
|
+
* in a different key order serialize alike.
|
|
161
|
+
*
|
|
162
|
+
* @param {unknown} value The value to serialize.
|
|
163
|
+
* @return {string} The stable JSON serialization of the value.
|
|
164
|
+
*/
|
|
165
|
+
function stableStringify(value) {
|
|
166
|
+
if (Array.isArray(value)) {
|
|
167
|
+
return `[${value.map(stableStringify).join(',')}]`;
|
|
168
|
+
}
|
|
169
|
+
if (value && typeof value === 'object') {
|
|
170
|
+
const entries = Object.keys(value)
|
|
171
|
+
.sort()
|
|
172
|
+
.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`);
|
|
173
|
+
return `{${entries.join(',')}}`;
|
|
174
|
+
}
|
|
175
|
+
return JSON.stringify(value) ?? 'null';
|
|
176
|
+
}
|
package/esm/utils/index.d.ts
CHANGED
package/esm/utils/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appweaver/client",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Appweaver - the backend framework for AI-first development (@client)",
|
|
5
5
|
"author": "Luka Matosevic",
|
|
6
6
|
"license": "MIT",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"access": "public"
|
|
50
50
|
},
|
|
51
51
|
"engines": {
|
|
52
|
-
"node": ">=
|
|
52
|
+
"node": ">= 22"
|
|
53
53
|
},
|
|
54
54
|
"scripts": {
|
|
55
55
|
"weaver-client": "node ./dist/cjs/weaver-client.js"
|