@obsidiane/meridiane 2.0.10 → 2.0.11
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 +19 -2
- package/cli.js +21 -37
- package/package.json +3 -3
- package/src/README.md +9 -0
- package/{tools/app → src/application}/bridge-workflow.js +15 -13
- package/src/application/generate-bridge.js +50 -0
- package/{tools/app → src/application}/run-build.js +1 -1
- package/{tools/app → src/application}/run-dev.js +30 -36
- package/{tools/app → src/application}/run-generate.js +3 -3
- package/{tools → src}/domain/bridge-options.js +8 -21
- package/src/domain/list-values.js +6 -0
- package/src/generators/bridge/write-bridge-files.js +100 -0
- package/src/generators/models/interface-schema.js +116 -0
- package/src/generators/models/model-selection.js +148 -0
- package/src/generators/models/oas-contract.js +170 -0
- package/{tools/generator → src/generators}/models/oas-schema-name.js +6 -15
- package/src/generators/models/openapi-to-models.js +136 -0
- package/{tools/generator → src/generators}/models/schema-utils.js +27 -3
- package/src/generators/models/templates/type-alias.hbs +2 -0
- package/src/generators/models/type-expression.js +65 -0
- package/src/generators/models/type-resolver.js +240 -0
- package/src/generators/models/utils.js +89 -0
- package/src/generators/models/write-model-files.js +55 -0
- package/src/infrastructure/dev-project.js +48 -0
- package/{tools/infra → src/infrastructure}/exec.js +3 -2
- package/src/infrastructure/local-package-install.js +104 -0
- package/src/infrastructure/spec-loader.js +65 -0
- package/{tools/infra → src/infrastructure}/standalone-workspace.js +31 -29
- package/templates/{_lib_template → angular-bridge}/README.md +9 -0
- package/templates/angular-bridge/src/lib/bridge/rest/api-platform.adapter.ts +82 -0
- package/templates/angular-bridge/src/lib/bridge/rest/http-request.options.ts +30 -0
- package/templates/{_lib_template → angular-bridge}/src/lib/bridge/rest/query-builder.ts +10 -10
- package/templates/angular-bridge/src/lib/bridge/sse/mercure-connections.ts +255 -0
- package/templates/angular-bridge/src/lib/bridge/sse/mercure-payload.ts +82 -0
- package/templates/angular-bridge/src/lib/bridge/sse/mercure-url.builder.ts +39 -0
- package/templates/angular-bridge/src/lib/bridge/sse/mercure.adapter.ts +358 -0
- package/templates/{_lib_template → angular-bridge}/src/lib/bridge/sse/ref-count-topic.registry.ts +3 -12
- package/templates/angular-bridge/src/lib/bridge/sse/subscription-claims.ts +76 -0
- package/templates/angular-bridge/src/lib/bridge-options.ts +59 -0
- package/templates/{_lib_template → angular-bridge}/src/lib/facades/bridge.facade.ts +50 -51
- package/templates/{_lib_template → angular-bridge}/src/lib/facades/facade.factory.ts +0 -1
- package/templates/angular-bridge/src/lib/facades/facade.interface.ts +13 -0
- package/templates/{_lib_template → angular-bridge}/src/lib/facades/resource.facade.ts +24 -31
- package/templates/angular-bridge/src/lib/interceptors/bearer-auth.interceptor.ts +33 -0
- package/templates/angular-bridge/src/lib/interceptors/bridge-defaults.interceptor.ts +65 -0
- package/templates/angular-bridge/src/lib/interceptors/content-type.interceptor.ts +37 -0
- package/templates/angular-bridge/src/lib/interceptors/scoped.interceptor.ts +9 -0
- package/templates/angular-bridge/src/lib/interceptors/singleflight.interceptor.ts +46 -0
- package/templates/{_lib_template → angular-bridge}/src/lib/ports/realtime.port.ts +6 -2
- package/templates/{_lib_template → angular-bridge}/src/lib/ports/resource-repository.port.ts +7 -7
- package/templates/angular-bridge/src/lib/provide-bridge.ts +98 -0
- package/templates/angular-bridge/src/lib/utils/iri.ts +6 -0
- package/templates/angular-bridge/src/lib/utils/request-url.ts +31 -0
- package/templates/{_lib_template → angular-bridge}/src/public-api.ts +2 -0
- package/templates/_lib_template/src/lib/bridge/rest/api-platform.adapter.ts +0 -84
- package/templates/_lib_template/src/lib/bridge/rest/http-request.options.ts +0 -21
- package/templates/_lib_template/src/lib/bridge/sse/mercure-url.builder.ts +0 -17
- package/templates/_lib_template/src/lib/bridge/sse/mercure.adapter.ts +0 -806
- package/templates/_lib_template/src/lib/facades/facade.interface.ts +0 -31
- package/templates/_lib_template/src/lib/interceptors/bridge-defaults.interceptor.ts +0 -53
- package/templates/_lib_template/src/lib/interceptors/content-type.interceptor.ts +0 -49
- package/templates/_lib_template/src/lib/interceptors/singleflight.interceptor.ts +0 -55
- package/templates/_lib_template/src/lib/provide-bridge.ts +0 -186
- package/tools/build.js +0 -1
- package/tools/dev.js +0 -1
- package/tools/domain/paths.js +0 -10
- package/tools/generate.js +0 -1
- package/tools/generator/models/oas-contract.js +0 -159
- package/tools/generator/models/openapi-to-models.js +0 -240
- package/tools/generator/models/type-resolver.js +0 -107
- package/tools/generator/models/utils.js +0 -62
- package/tools/infra/dev-support.js +0 -189
- package/tools/infra/generate/bridge.js +0 -212
- package/tools/infra/spec-loader.js +0 -59
- /package/{tools → src}/domain/lib-name.js +0 -0
- /package/{tools/generator → src/generators}/models/handlebars.js +0 -0
- /package/{tools/generator → src/generators}/models/templates/index.hbs +0 -0
- /package/{tools/generator → src/generators}/models/templates/model.hbs +0 -0
- /package/{tools/infra → src/infrastructure}/json.js +0 -0
- /package/{tools/infra → src/infrastructure}/logger.js +0 -0
- /package/templates/{_lib_template → angular-bridge}/ng-package.json +0 -0
- /package/templates/{_lib_template → angular-bridge}/package.json +0 -0
- /package/templates/{_lib_template → angular-bridge}/src/lib/bridge/sse/eventsource-wrapper.ts +0 -0
- /package/templates/{_lib_template → angular-bridge}/src/lib/bridge/sse/mercure-topic.mapper.ts +0 -0
- /package/templates/{_lib_template → angular-bridge}/src/lib/bridge.types.ts +0 -0
- /package/templates/{_lib_template → angular-bridge}/src/lib/interceptors/bridge-debug.interceptor.ts +0 -0
- /package/templates/{_lib_template → angular-bridge}/src/lib/tokens.ts +0 -0
- /package/templates/{_lib_template → angular-bridge}/src/lib/utils/url.ts +0 -0
- /package/templates/{_lib_template → angular-bridge}/src/models/index.ts +0 -0
- /package/templates/{_lib_template → angular-bridge}/tsconfig.lib.json +0 -0
- /package/templates/{_lib_template → angular-bridge}/tsconfig.lib.prod.json +0 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import {
|
|
2
|
+
canonicalizeMediaType,
|
|
3
|
+
collectUsedSchemasForMediaType,
|
|
4
|
+
expandWithSchemaDependencies,
|
|
5
|
+
} from './oas-contract.js';
|
|
6
|
+
import {
|
|
7
|
+
baseTsNameFromBasePath,
|
|
8
|
+
formatSuffixToken,
|
|
9
|
+
groupTsNameFromGroupPath,
|
|
10
|
+
parseOpenApiSchemaName,
|
|
11
|
+
} from './oas-schema-name.js';
|
|
12
|
+
import {sanitizeTypeName} from './utils.js';
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_MODEL_FORMAT = 'application/ld+json';
|
|
15
|
+
|
|
16
|
+
function normalizeFormats(values) {
|
|
17
|
+
const formats = [];
|
|
18
|
+
for (const value of Array.isArray(values) ? values : []) {
|
|
19
|
+
const format = canonicalizeMediaType(value);
|
|
20
|
+
if (format && !formats.includes(format)) formats.push(format);
|
|
21
|
+
}
|
|
22
|
+
return formats.length > 0 ? formats : [DEFAULT_MODEL_FORMAT];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function indexFormatsBySchema(spec, formats) {
|
|
26
|
+
const formatsBySchema = new Map();
|
|
27
|
+
|
|
28
|
+
for (const format of formats) {
|
|
29
|
+
const schemaNames = collectUsedSchemasForMediaType(spec, {mediaType: format});
|
|
30
|
+
for (const schemaName of schemaNames) {
|
|
31
|
+
if (!schemaName) continue;
|
|
32
|
+
const schemaFormats = formatsBySchema.get(schemaName) ?? new Set();
|
|
33
|
+
schemaFormats.add(format);
|
|
34
|
+
formatsBySchema.set(schemaName, schemaFormats);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return formatsBySchema;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function matchesRule(value, rule) {
|
|
42
|
+
if (rule instanceof RegExp) return rule.test(value);
|
|
43
|
+
if (typeof rule === 'function') return !!rule(value);
|
|
44
|
+
return typeof rule === 'string' && rule.length > 0 && value.includes(rule);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isSelected(schemaName, include, exclude) {
|
|
48
|
+
const parsed = parseOpenApiSchemaName(schemaName);
|
|
49
|
+
const canonicalName = parsed.groupPath ? `${parsed.basePath}-${parsed.groupPath}` : parsed.basePath;
|
|
50
|
+
const names = [schemaName, canonicalName];
|
|
51
|
+
const matchesAny = (rules) => rules.some((rule) =>
|
|
52
|
+
names.some((name) => matchesRule(name, rule))
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return (include.length === 0 || matchesAny(include)) && !matchesAny(exclude);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function selectCandidates(schemas, formatsBySchema, include, exclude) {
|
|
59
|
+
const selected = [...formatsBySchema.keys()]
|
|
60
|
+
.filter((name) => name && !/^Hydra/i.test(name))
|
|
61
|
+
.sort((left, right) => left.localeCompare(right))
|
|
62
|
+
.filter((name) => isSelected(name, include, exclude));
|
|
63
|
+
|
|
64
|
+
const candidates = include.length > 0 || exclude.length > 0
|
|
65
|
+
? expandWithSchemaDependencies(selected, schemas)
|
|
66
|
+
: new Set(selected);
|
|
67
|
+
|
|
68
|
+
return [...candidates]
|
|
69
|
+
.filter((name) => name && !/^Hydra/i.test(name))
|
|
70
|
+
.sort((left, right) => left.localeCompare(right));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function choosePreferredFormats(schemaNames, formats, formatsBySchema) {
|
|
74
|
+
const formatPriority = new Map(formats.map((format, index) => [format, index]));
|
|
75
|
+
const preferredFormats = new Map();
|
|
76
|
+
|
|
77
|
+
for (const schemaName of schemaNames) {
|
|
78
|
+
const schemaFormats = [...(formatsBySchema.get(schemaName) ?? [])];
|
|
79
|
+
const preferred = schemaFormats.sort(
|
|
80
|
+
(left, right) => (formatPriority.get(left) ?? 999) - (formatPriority.get(right) ?? 999)
|
|
81
|
+
)[0] ?? formats[0];
|
|
82
|
+
preferredFormats.set(schemaName, preferred);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {preferredFormats, formatPriority};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function desiredTypeName(schemaName) {
|
|
89
|
+
const parsed = parseOpenApiSchemaName(schemaName);
|
|
90
|
+
if (parsed.isMergePatch) return undefined;
|
|
91
|
+
|
|
92
|
+
const base = baseTsNameFromBasePath(parsed.basePath);
|
|
93
|
+
const group = groupTsNameFromGroupPath(parsed.groupPath);
|
|
94
|
+
return `${base}${group}` || 'Model';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function assignTypeNames(schemaNames, preferredFormats, formatPriority) {
|
|
98
|
+
const schemasByDesiredName = new Map();
|
|
99
|
+
|
|
100
|
+
for (const schemaName of schemaNames) {
|
|
101
|
+
const desiredName = desiredTypeName(schemaName);
|
|
102
|
+
if (!desiredName) continue;
|
|
103
|
+
|
|
104
|
+
const entries = schemasByDesiredName.get(desiredName) ?? [];
|
|
105
|
+
entries.push({schemaName, preferredFormat: preferredFormats.get(schemaName)});
|
|
106
|
+
schemasByDesiredName.set(desiredName, entries);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const usedNames = new Set();
|
|
110
|
+
const typeNames = new Map();
|
|
111
|
+
const sortedGroups = [...schemasByDesiredName.entries()]
|
|
112
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
113
|
+
|
|
114
|
+
for (const [desiredName, entries] of sortedGroups) {
|
|
115
|
+
entries.sort((left, right) => {
|
|
116
|
+
const byFormat =
|
|
117
|
+
(formatPriority.get(left.preferredFormat) ?? 999) -
|
|
118
|
+
(formatPriority.get(right.preferredFormat) ?? 999);
|
|
119
|
+
return byFormat || left.schemaName.localeCompare(right.schemaName);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
entries.forEach((entry, index) => {
|
|
123
|
+
const candidateName = index === 0
|
|
124
|
+
? desiredName
|
|
125
|
+
: `${desiredName}${formatSuffixToken(entry.preferredFormat)}`;
|
|
126
|
+
typeNames.set(entry.schemaName, sanitizeTypeName(candidateName, usedNames));
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return typeNames;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function selectModelSchemas(spec, options = {}) {
|
|
134
|
+
const schemas = spec?.components?.schemas ?? {};
|
|
135
|
+
const formats = normalizeFormats(options.formats);
|
|
136
|
+
const include = Array.isArray(options.includeSchemaNames) ? options.includeSchemaNames : [];
|
|
137
|
+
const exclude = Array.isArray(options.excludeSchemaNames) ? options.excludeSchemaNames : [];
|
|
138
|
+
const formatsBySchema = indexFormatsBySchema(spec, formats);
|
|
139
|
+
const schemaNames = selectCandidates(schemas, formatsBySchema, include, exclude);
|
|
140
|
+
const {preferredFormats, formatPriority} = choosePreferredFormats(
|
|
141
|
+
schemaNames,
|
|
142
|
+
formats,
|
|
143
|
+
formatsBySchema
|
|
144
|
+
);
|
|
145
|
+
const typeNames = assignTypeNames(schemaNames, preferredFormats, formatPriority);
|
|
146
|
+
|
|
147
|
+
return {schemas, formats, schemaNames, preferredFormats, typeNames};
|
|
148
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import {resolveLocalRef, schemaNameFromRef} from './schema-utils.js';
|
|
2
|
+
|
|
3
|
+
function deref(spec, maybeRef) {
|
|
4
|
+
if (!maybeRef || typeof maybeRef !== 'object') return maybeRef;
|
|
5
|
+
if (typeof maybeRef.$ref === 'string') return resolveLocalRef(spec, maybeRef.$ref) || maybeRef;
|
|
6
|
+
return maybeRef;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function canonicalizeMediaType(mediaType) {
|
|
10
|
+
return String(mediaType || '').split(';')[0].trim().toLowerCase();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getContentSchema(content, wantedLower) {
|
|
14
|
+
if (!content || typeof content !== 'object') return undefined;
|
|
15
|
+
for (const [mediaType, definition] of Object.entries(content)) {
|
|
16
|
+
if (canonicalizeMediaType(mediaType) === wantedLower) return definition?.schema;
|
|
17
|
+
}
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function is2xxStatus(code) {
|
|
22
|
+
const status = String(code || '');
|
|
23
|
+
if (/^[0-9]{3}$/.test(status)) return status.startsWith('2');
|
|
24
|
+
return /^2xx$/i.test(status);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const HTTP_METHODS = new Set(['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace']);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Collects only component schema refs (`#/components/schemas/*`) found in a JSON schema.
|
|
31
|
+
* @param {any} schema
|
|
32
|
+
* @param {Set<string>} out
|
|
33
|
+
*/
|
|
34
|
+
function collectComponentSchemaRefs(schema, out) {
|
|
35
|
+
const seen = new Set();
|
|
36
|
+
|
|
37
|
+
function walk(node) {
|
|
38
|
+
if (!node || typeof node !== 'object') return;
|
|
39
|
+
if (seen.has(node)) return;
|
|
40
|
+
seen.add(node);
|
|
41
|
+
|
|
42
|
+
if (node.$ref && typeof node.$ref === 'string') {
|
|
43
|
+
const name = schemaNameFromRef(node.$ref);
|
|
44
|
+
if (name) out.add(name);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (Array.isArray(node)) {
|
|
48
|
+
for (const item of node) walk(item);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const [key, value] of Object.entries(node)) {
|
|
53
|
+
if (key !== '$ref') walk(value);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
walk(schema);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function listSchemaRoots(spec, {mediaType} = {}) {
|
|
61
|
+
const wantedLower = canonicalizeMediaType(mediaType);
|
|
62
|
+
const roots = [];
|
|
63
|
+
const visited = new Set();
|
|
64
|
+
|
|
65
|
+
function addParameter(parameterOrRef) {
|
|
66
|
+
const parameter = deref(spec, parameterOrRef);
|
|
67
|
+
if (!parameter || typeof parameter !== 'object') return;
|
|
68
|
+
if (parameter.schema) roots.push(parameter.schema);
|
|
69
|
+
const contentSchema = getContentSchema(parameter.content, wantedLower);
|
|
70
|
+
if (contentSchema) roots.push(contentSchema);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function addHeaders(headers) {
|
|
74
|
+
for (const header of Object.values(headers ?? {})) addParameter(header);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function visitOperation(method, operationOrRef) {
|
|
78
|
+
const operation = deref(spec, operationOrRef);
|
|
79
|
+
if (!operation || typeof operation !== 'object') return;
|
|
80
|
+
if (visited.has(operation)) return;
|
|
81
|
+
visited.add(operation);
|
|
82
|
+
|
|
83
|
+
for (const parameter of operation.parameters ?? []) addParameter(parameter);
|
|
84
|
+
|
|
85
|
+
if (method !== 'patch') {
|
|
86
|
+
const requestBody = deref(spec, operation.requestBody);
|
|
87
|
+
const requestSchema = getContentSchema(requestBody?.content, wantedLower);
|
|
88
|
+
if (requestSchema) roots.push(requestSchema);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const [code, responseOrRef] of Object.entries(operation.responses ?? {})) {
|
|
92
|
+
if (!is2xxStatus(code) && String(code).toLowerCase() !== 'default') continue;
|
|
93
|
+
const response = deref(spec, responseOrRef);
|
|
94
|
+
const responseSchema = getContentSchema(response?.content, wantedLower);
|
|
95
|
+
if (responseSchema) roots.push(responseSchema);
|
|
96
|
+
addHeaders(response?.headers);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (const callbackOrRef of Object.values(operation.callbacks ?? {})) {
|
|
100
|
+
const callback = deref(spec, callbackOrRef);
|
|
101
|
+
for (const pathItem of Object.values(callback ?? {})) visitPathItem(pathItem);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function visitPathItem(pathItemOrRef) {
|
|
106
|
+
const pathItem = deref(spec, pathItemOrRef);
|
|
107
|
+
if (!pathItem || typeof pathItem !== 'object') return;
|
|
108
|
+
if (visited.has(pathItem)) return;
|
|
109
|
+
visited.add(pathItem);
|
|
110
|
+
|
|
111
|
+
for (const parameter of pathItem.parameters ?? []) addParameter(parameter);
|
|
112
|
+
for (const [method, operation] of Object.entries(pathItem)) {
|
|
113
|
+
const normalizedMethod = method.toLowerCase();
|
|
114
|
+
if (HTTP_METHODS.has(normalizedMethod)) visitOperation(normalizedMethod, operation);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function visitPaths(paths) {
|
|
119
|
+
for (const pathItem of Object.values(paths ?? {})) visitPathItem(pathItem);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
visitPaths(spec?.paths);
|
|
123
|
+
visitPaths(spec?.webhooks);
|
|
124
|
+
|
|
125
|
+
return roots;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Returns the transitive closure of schema dependencies (following component-schema $ref).
|
|
130
|
+
* @param {Iterable<string>} start
|
|
131
|
+
* @param {Record<string, any>} schemas
|
|
132
|
+
*/
|
|
133
|
+
export function expandWithSchemaDependencies(start, schemas) {
|
|
134
|
+
const out = new Set();
|
|
135
|
+
const queue = [];
|
|
136
|
+
|
|
137
|
+
for (const schemaName of start) {
|
|
138
|
+
if (!schemaName || out.has(schemaName)) continue;
|
|
139
|
+
out.add(schemaName);
|
|
140
|
+
queue.push(schemaName);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
while (queue.length) {
|
|
144
|
+
const name = queue.pop();
|
|
145
|
+
const schema = schemas[name];
|
|
146
|
+
if (!schema) continue;
|
|
147
|
+
const refs = new Set();
|
|
148
|
+
collectComponentSchemaRefs(schema, refs);
|
|
149
|
+
for (const dependency of refs) {
|
|
150
|
+
if (!dependency || out.has(dependency)) continue;
|
|
151
|
+
out.add(dependency);
|
|
152
|
+
queue.push(dependency);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Collects all component schemas used by endpoints for one media type.
|
|
161
|
+
* @param {any} spec
|
|
162
|
+
* @param {{ mediaType: string }} cfg
|
|
163
|
+
*/
|
|
164
|
+
export function collectUsedSchemasForMediaType(spec, cfg) {
|
|
165
|
+
const schemas = spec?.components?.schemas || {};
|
|
166
|
+
const roots = listSchemaRoots(spec, cfg);
|
|
167
|
+
const initialRefs = new Set();
|
|
168
|
+
for (const root of roots) collectComponentSchemaRefs(root, initialRefs);
|
|
169
|
+
return expandWithSchemaDependencies(initialRefs, schemas);
|
|
170
|
+
}
|
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
import { canonicalizeMediaType } from './oas-contract.js';
|
|
2
|
-
|
|
3
|
-
function pascalizeTokens(tokens) {
|
|
4
|
-
return tokens
|
|
5
|
-
.filter(Boolean)
|
|
6
|
-
.map((t) => String(t).replace(/^[^A-Za-z0-9]+|[^A-Za-z0-9]+$/g, ''))
|
|
7
|
-
.filter(Boolean)
|
|
8
|
-
.map((t) => t.charAt(0).toUpperCase() + t.slice(1))
|
|
9
|
-
.join('');
|
|
10
|
-
}
|
|
2
|
+
import {pascalCase} from './utils.js';
|
|
11
3
|
|
|
12
4
|
export function parseOpenApiSchemaName(originalName) {
|
|
13
5
|
const raw = String(originalName || '');
|
|
@@ -50,22 +42,21 @@ export function parseOpenApiSchemaName(originalName) {
|
|
|
50
42
|
export function baseTsNameFromBasePath(basePath) {
|
|
51
43
|
const parts = String(basePath || '').split('.').filter(Boolean);
|
|
52
44
|
if (parts.length === 0) return 'Model';
|
|
53
|
-
const a =
|
|
45
|
+
const a = pascalCase([parts[0]]);
|
|
54
46
|
if (parts.length === 1) return a;
|
|
55
|
-
const b =
|
|
47
|
+
const b = pascalCase([parts[1]]);
|
|
56
48
|
if (parts.length === 2 && b.toLowerCase().startsWith(a.toLowerCase())) return b;
|
|
57
|
-
return parts.map((
|
|
49
|
+
return parts.map((part) => pascalCase([part])).join('');
|
|
58
50
|
}
|
|
59
51
|
|
|
60
52
|
export function groupTsNameFromGroupPath(groupPath) {
|
|
61
53
|
const tokens = String(groupPath || '').split(/[._:]+/g).filter(Boolean);
|
|
62
|
-
return
|
|
54
|
+
return pascalCase(tokens);
|
|
63
55
|
}
|
|
64
56
|
|
|
65
57
|
export function formatSuffixToken(mediaType) {
|
|
66
58
|
const canon = canonicalizeMediaType(mediaType);
|
|
67
59
|
const tokens = canon.split(/[\/+.-]+/g).filter(Boolean);
|
|
68
60
|
const trimmed = tokens[0] === 'application' ? tokens.slice(1) : tokens;
|
|
69
|
-
return
|
|
61
|
+
return pascalCase(trimmed.length ? trimmed : tokens) || 'Fmt';
|
|
70
62
|
}
|
|
71
|
-
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import {canonicalizeMediaType} from './oas-contract.js';
|
|
2
|
+
import {collectInterfaceTypeNames, resolveInterfaceSchema} from './interface-schema.js';
|
|
3
|
+
import {DEFAULT_MODEL_FORMAT, selectModelSchemas} from './model-selection.js';
|
|
4
|
+
import {isObject} from './schema-utils.js';
|
|
5
|
+
import {createTypeResolver} from './type-resolver.js';
|
|
6
|
+
import {quoteKeyIfNeeded} from './utils.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {{
|
|
10
|
+
* requiredMode?: 'all'|'spec',
|
|
11
|
+
* formats?: string[],
|
|
12
|
+
* includeSchemaNames?: Array<RegExp|string|((name: string) => boolean)>,
|
|
13
|
+
* excludeSchemaNames?: Array<RegExp|string|((name: string) => boolean)>,
|
|
14
|
+
* }} BuildOptions
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** @typedef {{name: string, tsKey: string, type: string, optional: boolean}} ModelProp */
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {{
|
|
21
|
+
* name: string,
|
|
22
|
+
* props: ModelProp[],
|
|
23
|
+
* imports: string[],
|
|
24
|
+
* extendsTypes: string[],
|
|
25
|
+
* extendsItem?: boolean,
|
|
26
|
+
* isAlias?: boolean,
|
|
27
|
+
* type?: string,
|
|
28
|
+
* }} ModelDefinition
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
function normalizeRequiredMode(value) {
|
|
32
|
+
return String(value ?? '').trim().toLowerCase() === 'spec' ? 'spec' : 'all';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function createModelBuilder({spec, schemas, typeNames, interfaceTypeNames, requiredMode}) {
|
|
36
|
+
const knownTypeNames = new Set(typeNames.values());
|
|
37
|
+
|
|
38
|
+
function importsFor(dependencies, currentTypeName) {
|
|
39
|
+
return [...dependencies]
|
|
40
|
+
.filter((dependency) => dependency !== currentTypeName && knownTypeNames.has(dependency))
|
|
41
|
+
.sort((left, right) => left.localeCompare(right));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function resolverFor(dependencies) {
|
|
45
|
+
return createTypeResolver({dependencies, typeNames, requiredMode, spec});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function buildAlias(raw, typeName) {
|
|
49
|
+
const dependencies = new Set();
|
|
50
|
+
const type = resolverFor(dependencies).typeOf(raw);
|
|
51
|
+
return {
|
|
52
|
+
name: typeName,
|
|
53
|
+
type,
|
|
54
|
+
imports: importsFor(dependencies, typeName),
|
|
55
|
+
isAlias: true,
|
|
56
|
+
props: [],
|
|
57
|
+
extendsTypes: [],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function buildInterface(schema, typeName, extendsTypes, preferredFormat) {
|
|
62
|
+
const isJsonLd = canonicalizeMediaType(preferredFormat) === DEFAULT_MODEL_FORMAT;
|
|
63
|
+
const requiredProperties = new Set(schema.required ?? []);
|
|
64
|
+
const dependencies = new Set(extendsTypes);
|
|
65
|
+
const resolver = resolverFor(dependencies);
|
|
66
|
+
|
|
67
|
+
const props = Object.entries(schema.properties ?? {})
|
|
68
|
+
.filter(([propertyName]) => !isJsonLd || !['@id', '@type', '@context'].includes(propertyName))
|
|
69
|
+
.map(([propertyName, propertySchema]) => ({
|
|
70
|
+
name: propertyName,
|
|
71
|
+
tsKey: quoteKeyIfNeeded(propertyName),
|
|
72
|
+
type: requiredMode === 'spec'
|
|
73
|
+
? resolver.typeOf(propertySchema)
|
|
74
|
+
: resolver.nullableTypeOf(propertySchema),
|
|
75
|
+
optional: requiredMode === 'spec' ? !requiredProperties.has(propertyName) : true,
|
|
76
|
+
}));
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
name: typeName,
|
|
80
|
+
props,
|
|
81
|
+
imports: importsFor(dependencies, typeName),
|
|
82
|
+
extendsTypes,
|
|
83
|
+
extendsItem: isJsonLd,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return function buildModel(schemaName, preferredFormat) {
|
|
88
|
+
const raw = schemas[schemaName];
|
|
89
|
+
const typeName = typeNames.get(schemaName);
|
|
90
|
+
if (!typeName || raw === undefined) return undefined;
|
|
91
|
+
|
|
92
|
+
const {schema, extendsTypes, compatible} = resolveInterfaceSchema(
|
|
93
|
+
raw,
|
|
94
|
+
typeName,
|
|
95
|
+
typeNames,
|
|
96
|
+
interfaceTypeNames
|
|
97
|
+
);
|
|
98
|
+
const isObjectLike = schema?.type === 'object' || isObject(schema?.properties) || extendsTypes.length > 0;
|
|
99
|
+
if (!compatible || !isObjectLike || !interfaceTypeNames.has(typeName)) {
|
|
100
|
+
return buildAlias(raw, typeName);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return buildInterface(schema, typeName, extendsTypes, preferredFormat);
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Builds TypeScript model definitions from OpenAPI `components.schemas`.
|
|
109
|
+
* @param {any} spec
|
|
110
|
+
* @param {BuildOptions} [options]
|
|
111
|
+
* @returns {{models: ModelDefinition[]}}
|
|
112
|
+
*/
|
|
113
|
+
export function buildModelsFromOpenAPI(spec, options = {}) {
|
|
114
|
+
const requiredMode = normalizeRequiredMode(options.requiredMode);
|
|
115
|
+
const {schemas, formats, schemaNames, preferredFormats, typeNames} = selectModelSchemas(
|
|
116
|
+
spec,
|
|
117
|
+
options
|
|
118
|
+
);
|
|
119
|
+
const interfaceTypeNames = collectInterfaceTypeNames(schemas, schemaNames, typeNames);
|
|
120
|
+
const buildModel = createModelBuilder({
|
|
121
|
+
spec,
|
|
122
|
+
schemas,
|
|
123
|
+
typeNames,
|
|
124
|
+
interfaceTypeNames,
|
|
125
|
+
requiredMode,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const models = [];
|
|
129
|
+
for (const schemaName of schemaNames) {
|
|
130
|
+
const preferredFormat = preferredFormats.get(schemaName) ?? formats[0];
|
|
131
|
+
const model = buildModel(schemaName, preferredFormat);
|
|
132
|
+
if (model) models.push(model);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {models};
|
|
136
|
+
}
|
|
@@ -10,14 +10,35 @@ export function isObject(v) {
|
|
|
10
10
|
return !!v && typeof v === 'object' && !Array.isArray(v);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
export function hasOwn(object, key) {
|
|
14
|
+
return Object.prototype.hasOwnProperty.call(object, key);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function decodeJsonPointerSegment(segment) {
|
|
18
|
+
return segment.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function resolveLocalRef(document, ref) {
|
|
22
|
+
const pointer = String(ref ?? '');
|
|
23
|
+
if (pointer === '#') return document;
|
|
24
|
+
if (!pointer.startsWith('#/')) return undefined;
|
|
25
|
+
|
|
26
|
+
let current = document;
|
|
27
|
+
for (const segment of pointer.slice(2).split('/').map(decodeJsonPointerSegment)) {
|
|
28
|
+
if (!current || typeof current !== 'object') return undefined;
|
|
29
|
+
current = current[segment];
|
|
30
|
+
}
|
|
31
|
+
return current;
|
|
32
|
+
}
|
|
33
|
+
|
|
13
34
|
/**
|
|
14
35
|
* Extrait le nom du schéma depuis une référence $ref
|
|
15
36
|
* @param {string} $ref
|
|
16
37
|
* @returns {string|undefined}
|
|
17
38
|
*/
|
|
18
39
|
export function schemaNameFromRef($ref) {
|
|
19
|
-
const
|
|
20
|
-
return
|
|
40
|
+
const match = String($ref ?? '').match(/^#\/components\/schemas\/([^/]+)$/);
|
|
41
|
+
return match ? decodeJsonPointerSegment(match[1]) : undefined;
|
|
21
42
|
}
|
|
22
43
|
|
|
23
44
|
/**
|
|
@@ -38,7 +59,10 @@ export function mergeAllOf(schemas) {
|
|
|
38
59
|
return null;
|
|
39
60
|
}
|
|
40
61
|
if (s.type === 'object') {
|
|
41
|
-
Object.
|
|
62
|
+
for (const [name, property] of Object.entries(s.properties ?? {})) {
|
|
63
|
+
if (hasOwn(out.properties, name)) return null;
|
|
64
|
+
out.properties[name] = property;
|
|
65
|
+
}
|
|
42
66
|
if (Array.isArray(s.required)) out.required.push(...s.required);
|
|
43
67
|
} else {
|
|
44
68
|
return null;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const PRECEDENCE = {
|
|
2
|
+
union: 1,
|
|
3
|
+
intersection: 2,
|
|
4
|
+
primary: 3,
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
function expression(code, precedence, kind, members, acceptsNull) {
|
|
8
|
+
return {code, precedence, kind, members, acceptsNull};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function atomicType(code, acceptsNull = false) {
|
|
12
|
+
return expression(code, PRECEDENCE.primary, 'atomic', [], acceptsNull);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function formatType(type, parentPrecedence = PRECEDENCE.primary) {
|
|
16
|
+
return type.precedence < parentPrecedence ? `(${type.code})` : type.code;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function uniqueTypes(types) {
|
|
20
|
+
return [...new Map(types.map((type) => [type.code, type])).values()];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function flatten(types, kind) {
|
|
24
|
+
return types.flatMap((type) => type.kind === kind ? type.members : [type]);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function unionType(types) {
|
|
28
|
+
const members = uniqueTypes(flatten(types, 'union')).filter((member) => member.code !== 'never');
|
|
29
|
+
if (members.length === 0) return atomicType('any');
|
|
30
|
+
if (members.some((member) => member.code === 'any')) return atomicType('any');
|
|
31
|
+
if (members.length === 1) return members[0];
|
|
32
|
+
|
|
33
|
+
const ordered = members.filter((member) => member.code !== 'null');
|
|
34
|
+
ordered.push(...members.filter((member) => member.code === 'null'));
|
|
35
|
+
return expression(
|
|
36
|
+
ordered.map((member) => formatType(member, PRECEDENCE.union)).join(' | '),
|
|
37
|
+
PRECEDENCE.union,
|
|
38
|
+
'union',
|
|
39
|
+
ordered,
|
|
40
|
+
members.some((member) => member.acceptsNull)
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function intersectionType(types) {
|
|
45
|
+
const members = uniqueTypes(flatten(types, 'intersection')).filter((member) => member.code !== 'any');
|
|
46
|
+
if (members.length === 0) return atomicType('any');
|
|
47
|
+
if (members.some((member) => member.code === 'never')) return atomicType('never');
|
|
48
|
+
if (members.length === 1) return members[0];
|
|
49
|
+
|
|
50
|
+
return expression(
|
|
51
|
+
members.map((member) => formatType(member, PRECEDENCE.intersection)).join(' & '),
|
|
52
|
+
PRECEDENCE.intersection,
|
|
53
|
+
'intersection',
|
|
54
|
+
members,
|
|
55
|
+
members.every((member) => member.acceptsNull)
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function arrayType(item) {
|
|
60
|
+
return atomicType(`${formatType(item)}[]`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function nullableType(type) {
|
|
64
|
+
return type.acceptsNull ? type : unionType([type, atomicType('null', true)]);
|
|
65
|
+
}
|