@obsidiane/meridiane 2.0.13 → 3.0.1
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 +33 -66
- package/cli.js +1 -10
- package/package.json +1 -1
- package/src/application/bridge-workflow.js +23 -66
- package/src/application/generate-bridge.js +32 -36
- package/src/application/run-build.js +8 -30
- package/src/application/run-dev.js +8 -63
- package/src/application/run-generate.js +8 -47
- package/src/domain/bridge-options.js +10 -63
- package/src/generators/bridge/write-bridge-files.js +11 -29
- package/src/generators/models/interface-schema.js +3 -20
- package/src/generators/models/model-selection.js +67 -106
- package/src/generators/models/oas-contract.js +1 -108
- package/src/generators/models/oas-schema-name.js +1 -19
- package/src/generators/models/openapi-to-models.js +51 -42
- package/src/generators/models/schema-utils.js +0 -34
- package/src/generators/models/type-resolver.js +2 -20
- package/src/generators/models/utils.js +0 -11
- package/src/generators/models/write-model-files.js +5 -15
- package/src/infrastructure/dev-project.js +0 -33
- package/src/infrastructure/files.js +15 -0
- package/src/infrastructure/local-package-install.js +2 -2
- package/src/infrastructure/logger.js +0 -2
- package/templates/angular-bridge/README.md +29 -281
- package/templates/angular-bridge/package.json +2 -2
- package/templates/angular-bridge/src/lib/bridge/rest/http-request.options.ts +10 -19
- package/templates/angular-bridge/src/lib/bridge/sse/mercure-hub.ts +449 -0
- package/templates/angular-bridge/src/lib/bridge/sse/mercure-url.ts +28 -0
- package/templates/angular-bridge/src/lib/bridge/sse/resource-topic.ts +83 -0
- package/templates/angular-bridge/src/lib/bridge-options.ts +21 -17
- package/templates/angular-bridge/src/lib/bridge.types.ts +22 -33
- package/templates/angular-bridge/src/lib/facades/bridge.facade.ts +58 -134
- package/templates/angular-bridge/src/lib/facades/facade.factory.ts +25 -25
- package/templates/angular-bridge/src/lib/facades/facade.interface.ts +4 -9
- package/templates/angular-bridge/src/lib/facades/resource.facade.ts +38 -61
- package/templates/angular-bridge/src/lib/interceptors/bearer-auth.interceptor.ts +8 -12
- package/templates/angular-bridge/src/lib/interceptors/bridge-defaults.interceptor.ts +19 -11
- package/templates/angular-bridge/src/lib/interceptors/content-type.interceptor.ts +5 -17
- package/templates/angular-bridge/src/lib/interceptors/singleflight.interceptor.ts +28 -24
- package/templates/angular-bridge/src/lib/ports/realtime.port.ts +34 -53
- package/templates/angular-bridge/src/lib/ports/resource-repository.port.ts +31 -21
- package/templates/angular-bridge/src/lib/provide-bridge.ts +14 -38
- package/templates/angular-bridge/src/lib/tokens.ts +4 -15
- package/templates/angular-bridge/src/lib/utils/request-url.ts +4 -6
- package/templates/angular-bridge/src/lib/utils/url.ts +32 -9
- package/templates/angular-bridge/src/public-api.ts +0 -1
- package/templates/angular-bridge/tsconfig.lib.json +0 -3
- package/src/README.md +0 -9
- package/src/infrastructure/json.js +0 -71
- package/templates/angular-bridge/src/lib/bridge/rest/api-platform.adapter.ts +0 -82
- package/templates/angular-bridge/src/lib/bridge/sse/eventsource-wrapper.ts +0 -70
- package/templates/angular-bridge/src/lib/bridge/sse/mercure-connections.ts +0 -255
- package/templates/angular-bridge/src/lib/bridge/sse/mercure-payload.ts +0 -82
- package/templates/angular-bridge/src/lib/bridge/sse/mercure-topic.mapper.ts +0 -48
- package/templates/angular-bridge/src/lib/bridge/sse/mercure-url.builder.ts +0 -39
- package/templates/angular-bridge/src/lib/bridge/sse/mercure.adapter.ts +0 -358
- package/templates/angular-bridge/src/lib/bridge/sse/ref-count-topic.registry.ts +0 -36
- package/templates/angular-bridge/src/lib/bridge/sse/subscription-claims.ts +0 -76
- package/templates/angular-bridge/src/lib/utils/iri.ts +0 -6
|
@@ -2,32 +2,16 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
|
|
5
|
-
import { readJson,
|
|
5
|
+
import { ensureCleanDir, readJson, writeJson } from '../../infrastructure/files.js';
|
|
6
6
|
|
|
7
7
|
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
8
8
|
const packageRoot = path.resolve(currentDir, '../../..');
|
|
9
9
|
const bridgeTemplateDir = path.join(packageRoot, 'templates', 'angular-bridge');
|
|
10
10
|
|
|
11
|
-
export async function writeBridgeFiles({
|
|
12
|
-
projectDir
|
|
13
|
-
libName,
|
|
14
|
-
packageName,
|
|
15
|
-
version,
|
|
16
|
-
projectReadmePath,
|
|
17
|
-
debug,
|
|
18
|
-
log,
|
|
19
|
-
}) {
|
|
20
|
-
if (debug) {
|
|
21
|
-
log?.debug?.('[meridiane] generate lib', {
|
|
22
|
-
libName,
|
|
23
|
-
packageName,
|
|
24
|
-
version,
|
|
25
|
-
targetDir: projectDir,
|
|
26
|
-
});
|
|
27
|
-
}
|
|
11
|
+
export async function writeBridgeFiles({ projectDir, libName, packageName, version, projectReadmePath, log }) {
|
|
12
|
+
log.debug('[meridiane] generate lib', { libName, packageName, version, targetDir: projectDir });
|
|
28
13
|
|
|
29
|
-
await
|
|
30
|
-
await fs.mkdir(projectDir, { recursive: true });
|
|
14
|
+
await ensureCleanDir(projectDir);
|
|
31
15
|
await fs.cp(bridgeTemplateDir, projectDir, { recursive: true });
|
|
32
16
|
|
|
33
17
|
await replacePlaceholders(projectDir, {
|
|
@@ -39,9 +23,9 @@ export async function writeBridgeFiles({
|
|
|
39
23
|
const packageJson = await readJson(packageJsonPath);
|
|
40
24
|
packageJson.name = packageName;
|
|
41
25
|
packageJson.version = version;
|
|
42
|
-
await
|
|
26
|
+
await writeJson(packageJsonPath, packageJson);
|
|
43
27
|
|
|
44
|
-
await appendProjectReadme(path.join(projectDir, 'README.md'), projectReadmePath,
|
|
28
|
+
await appendProjectReadme(path.join(projectDir, 'README.md'), projectReadmePath, log);
|
|
45
29
|
}
|
|
46
30
|
|
|
47
31
|
export async function configureBridgeOutput({ projectDir, distRoot, libName, log }) {
|
|
@@ -50,8 +34,8 @@ export async function configureBridgeOutput({ projectDir, distRoot, libName, log
|
|
|
50
34
|
const outputDir = path.join(distRoot, libName);
|
|
51
35
|
|
|
52
36
|
ngPackage.dest = path.relative(projectDir, outputDir).split(path.sep).join('/');
|
|
53
|
-
await
|
|
54
|
-
log
|
|
37
|
+
await writeJson(ngPackagePath, ngPackage);
|
|
38
|
+
log.debug('ng-package.json dest overridden', { dest: ngPackage.dest });
|
|
55
39
|
}
|
|
56
40
|
|
|
57
41
|
async function replacePlaceholders(directory, placeholders) {
|
|
@@ -72,23 +56,21 @@ async function replacePlaceholders(directory, placeholders) {
|
|
|
72
56
|
}
|
|
73
57
|
}
|
|
74
58
|
|
|
75
|
-
async function appendProjectReadme(targetPath, sourcePath,
|
|
76
|
-
if (!sourcePath) return;
|
|
77
|
-
|
|
59
|
+
async function appendProjectReadme(targetPath, sourcePath, log) {
|
|
78
60
|
const projectReadme = await readText(sourcePath);
|
|
79
61
|
if (!projectReadme?.trim()) return;
|
|
80
62
|
|
|
81
63
|
const bridgeReadme = await readText(targetPath);
|
|
82
64
|
if (!bridgeReadme?.trim()) {
|
|
83
65
|
await fs.writeFile(targetPath, projectReadme);
|
|
84
|
-
|
|
66
|
+
log.debug('[meridiane] project README copied', { source: sourcePath });
|
|
85
67
|
return;
|
|
86
68
|
}
|
|
87
69
|
|
|
88
70
|
const combinedReadme =
|
|
89
71
|
`${bridgeReadme.trimEnd()}\n\n---\n\n## Documentation du projet\n\n${projectReadme.trimStart()}`;
|
|
90
72
|
await fs.writeFile(targetPath, combinedReadme);
|
|
91
|
-
|
|
73
|
+
log.debug('[meridiane] project README appended', { source: sourcePath });
|
|
92
74
|
}
|
|
93
75
|
|
|
94
76
|
async function readText(filePath) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {hasOwn, isObject,
|
|
1
|
+
import {hasOwn, isObject, schemaNameFromRef} from './schema-utils.js';
|
|
2
2
|
|
|
3
3
|
function hasPropertyConflict(left, right) {
|
|
4
4
|
return Object.keys(right).some((propertyName) => hasOwn(left, propertyName));
|
|
@@ -55,29 +55,12 @@ export function collectInterfaceTypeNames(schemas, schemaNames, typeNames) {
|
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
57
|
* Flattens object-only allOf parts and keeps component references as interface
|
|
58
|
-
* inheritance
|
|
58
|
+
* inheritance (Hydra base schemas are covered by `Item`). Unsafe or conflicting
|
|
59
|
+
* compositions fall back to a type alias.
|
|
59
60
|
*/
|
|
60
61
|
export function resolveInterfaceSchema(raw, currentTypeName, typeNames, interfaceTypeNames) {
|
|
61
62
|
if (!raw?.allOf) return {schema: raw, extendsTypes: [], compatible: true};
|
|
62
63
|
|
|
63
|
-
const merged = mergeAllOf(raw.allOf);
|
|
64
|
-
if (merged) {
|
|
65
|
-
const ownProperties = raw.properties ?? {};
|
|
66
|
-
if (hasPropertyConflict(merged.properties, ownProperties)) {
|
|
67
|
-
return {schema: raw, extendsTypes: [], compatible: false};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return {
|
|
71
|
-
schema: {
|
|
72
|
-
...merged,
|
|
73
|
-
properties: {...ownProperties, ...merged.properties},
|
|
74
|
-
required: [...new Set([...(raw.required ?? []), ...merged.required])],
|
|
75
|
-
},
|
|
76
|
-
extendsTypes: [],
|
|
77
|
-
compatible: true,
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
64
|
const properties = {...(raw.properties ?? {})};
|
|
82
65
|
const required = new Set(raw.required ?? []);
|
|
83
66
|
const inheritedTypes = new Set();
|
|
@@ -1,42 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
canonicalizeMediaType,
|
|
3
|
-
collectUsedSchemasForMediaType,
|
|
4
|
-
expandWithSchemaDependencies,
|
|
5
|
-
} from './oas-contract.js';
|
|
1
|
+
import {expandWithSchemaDependencies} from './oas-contract.js';
|
|
6
2
|
import {
|
|
7
3
|
baseTsNameFromBasePath,
|
|
8
|
-
formatSuffixToken,
|
|
9
4
|
groupTsNameFromGroupPath,
|
|
10
5
|
parseOpenApiSchemaName,
|
|
11
6
|
} from './oas-schema-name.js';
|
|
12
7
|
import {sanitizeTypeName} from './utils.js';
|
|
13
8
|
|
|
14
|
-
|
|
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
|
-
}
|
|
9
|
+
const HYDRA_SCHEMA = /^Hydra/i;
|
|
37
10
|
|
|
38
|
-
|
|
39
|
-
|
|
11
|
+
/** Representations of one resource, in emission order: the JSON-LD one is what the bridge reads. */
|
|
12
|
+
const VARIANT_ORDER = ['jsonld', 'none', 'multipart'];
|
|
13
|
+
const VARIANT_SUFFIX = {jsonld: 'JsonLd', none: 'Json', multipart: 'Multipart'};
|
|
40
14
|
|
|
41
15
|
function matchesRule(value, rule) {
|
|
42
16
|
if (rule instanceof RegExp) return rule.test(value);
|
|
@@ -55,94 +29,81 @@ function isSelected(schemaName, include, exclude) {
|
|
|
55
29
|
return (include.length === 0 || matchesAny(include)) && !matchesAny(exclude);
|
|
56
30
|
}
|
|
57
31
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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));
|
|
32
|
+
/** Hydra schemas are covered by the runtime `Item`/`Collection` types; merge-patch bodies by `Partial<T>`. */
|
|
33
|
+
function isModelSchema(schemaName) {
|
|
34
|
+
return !HYDRA_SCHEMA.test(schemaName) && !parseOpenApiSchemaName(schemaName).isMergePatch;
|
|
71
35
|
}
|
|
72
36
|
|
|
73
|
-
function
|
|
74
|
-
const
|
|
75
|
-
|
|
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};
|
|
37
|
+
function variantRank(schemaName) {
|
|
38
|
+
const rank = VARIANT_ORDER.indexOf(parseOpenApiSchemaName(schemaName).baseVariant);
|
|
39
|
+
return rank < 0 ? VARIANT_ORDER.length : rank;
|
|
86
40
|
}
|
|
87
41
|
|
|
88
|
-
|
|
42
|
+
/** Name shared by every representation of a resource (`Book`, `Book.jsonld`, `Book.multipart` → `Book`). */
|
|
43
|
+
export function resourceTypeName(schemaName) {
|
|
89
44
|
const parsed = parseOpenApiSchemaName(schemaName);
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const base = baseTsNameFromBasePath(parsed.basePath);
|
|
93
|
-
const group = groupTsNameFromGroupPath(parsed.groupPath);
|
|
94
|
-
return `${base}${group}` || 'Model';
|
|
45
|
+
return `${baseTsNameFromBasePath(parsed.basePath)}${groupTsNameFromGroupPath(parsed.groupPath)}` || 'Model';
|
|
95
46
|
}
|
|
96
47
|
|
|
97
|
-
|
|
98
|
-
|
|
48
|
+
/** Selects the component schemas of the contract, optionally filtered by `include` / `exclude`. */
|
|
49
|
+
export function selectModelSchemas(spec, options = {}) {
|
|
50
|
+
const schemas = spec?.components?.schemas ?? {};
|
|
51
|
+
const include = Array.isArray(options.includeSchemaNames) ? options.includeSchemaNames : [];
|
|
52
|
+
const exclude = Array.isArray(options.excludeSchemaNames) ? options.excludeSchemaNames : [];
|
|
99
53
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
54
|
+
const selected = Object.keys(schemas)
|
|
55
|
+
.filter(isModelSchema)
|
|
56
|
+
.filter((schemaName) => isSelected(schemaName, include, exclude));
|
|
57
|
+
const schemaNames = include.length > 0 || exclude.length > 0
|
|
58
|
+
? [...expandWithSchemaDependencies(selected, schemas)]
|
|
59
|
+
: selected;
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
schemas,
|
|
63
|
+
schemaNames: schemaNames.filter(isModelSchema).sort((left, right) => left.localeCompare(right)),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
103
66
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Names the models: the representations of one resource that have the same TypeScript shape share one model,
|
|
69
|
+
* named after the resource. A representation with a different shape gets its variant as suffix.
|
|
70
|
+
*
|
|
71
|
+
* @param {string[]} schemaNames
|
|
72
|
+
* @param {Map<string, string>} signatures shape of each schema, computed with resource names
|
|
73
|
+
* @returns {{typeNames: Map<string, string>, emitted: string[]}}
|
|
74
|
+
*/
|
|
75
|
+
export function assignTypeNames(schemaNames, signatures) {
|
|
76
|
+
const representations = new Map();
|
|
77
|
+
for (const schemaName of schemaNames) {
|
|
78
|
+
const resourceName = resourceTypeName(schemaName);
|
|
79
|
+
representations.set(resourceName, [...(representations.get(resourceName) ?? []), schemaName]);
|
|
107
80
|
}
|
|
108
81
|
|
|
109
82
|
const usedNames = new Set();
|
|
110
83
|
const typeNames = new Map();
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
for (const [
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
84
|
+
const emitted = [];
|
|
85
|
+
const sortedResources = [...representations.entries()].sort(([left], [right]) => left.localeCompare(right));
|
|
86
|
+
|
|
87
|
+
for (const [resourceName, names] of sortedResources) {
|
|
88
|
+
names.sort((left, right) => variantRank(left) - variantRank(right) || left.localeCompare(right));
|
|
89
|
+
const typeNameBySignature = new Map();
|
|
90
|
+
|
|
91
|
+
for (const schemaName of names) {
|
|
92
|
+
const signature = signatures.get(schemaName);
|
|
93
|
+
const sameShape = typeNameBySignature.get(signature);
|
|
94
|
+
if (sameShape) {
|
|
95
|
+
typeNames.set(schemaName, sameShape);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const variant = parseOpenApiSchemaName(schemaName).baseVariant;
|
|
100
|
+
const candidate = typeNameBySignature.size === 0 ? resourceName : `${resourceName}${VARIANT_SUFFIX[variant] ?? ''}`;
|
|
101
|
+
const typeName = sanitizeTypeName(candidate, usedNames);
|
|
102
|
+
typeNameBySignature.set(signature, typeName);
|
|
103
|
+
typeNames.set(schemaName, typeName);
|
|
104
|
+
emitted.push(schemaName);
|
|
105
|
+
}
|
|
128
106
|
}
|
|
129
107
|
|
|
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};
|
|
108
|
+
return {typeNames, emitted};
|
|
148
109
|
}
|
|
@@ -1,30 +1,4 @@
|
|
|
1
|
-
import {
|
|
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']);
|
|
1
|
+
import {schemaNameFromRef} from './schema-utils.js';
|
|
28
2
|
|
|
29
3
|
/**
|
|
30
4
|
* Collects only component schema refs (`#/components/schemas/*`) found in a JSON schema.
|
|
@@ -57,74 +31,6 @@ function collectComponentSchemaRefs(schema, out) {
|
|
|
57
31
|
walk(schema);
|
|
58
32
|
}
|
|
59
33
|
|
|
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
34
|
/**
|
|
129
35
|
* Returns the transitive closure of schema dependencies (following component-schema $ref).
|
|
130
36
|
* @param {Iterable<string>} start
|
|
@@ -155,16 +61,3 @@ export function expandWithSchemaDependencies(start, schemas) {
|
|
|
155
61
|
|
|
156
62
|
return out;
|
|
157
63
|
}
|
|
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,4 +1,3 @@
|
|
|
1
|
-
import { canonicalizeMediaType } from './oas-contract.js';
|
|
2
1
|
import {pascalCase} from './utils.js';
|
|
3
2
|
|
|
4
3
|
export function parseOpenApiSchemaName(originalName) {
|
|
@@ -26,17 +25,7 @@ export function parseOpenApiSchemaName(originalName) {
|
|
|
26
25
|
groupRaw = groupRaw.replace(/\.jsonmergepatch$/i, '');
|
|
27
26
|
}
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
const groupPath = groupRaw;
|
|
31
|
-
|
|
32
|
-
return {
|
|
33
|
-
originalName: raw,
|
|
34
|
-
basePath,
|
|
35
|
-
groupPath,
|
|
36
|
-
baseVariant,
|
|
37
|
-
isMergePatch,
|
|
38
|
-
key: `${basePath.toLowerCase()}|${groupPath.toLowerCase()}`,
|
|
39
|
-
};
|
|
28
|
+
return {basePath: baseRaw, groupPath: groupRaw, baseVariant, isMergePatch};
|
|
40
29
|
}
|
|
41
30
|
|
|
42
31
|
export function baseTsNameFromBasePath(basePath) {
|
|
@@ -53,10 +42,3 @@ export function groupTsNameFromGroupPath(groupPath) {
|
|
|
53
42
|
const tokens = String(groupPath || '').split(/[._:]+/g).filter(Boolean);
|
|
54
43
|
return pascalCase(tokens);
|
|
55
44
|
}
|
|
56
|
-
|
|
57
|
-
export function formatSuffixToken(mediaType) {
|
|
58
|
-
const canon = canonicalizeMediaType(mediaType);
|
|
59
|
-
const tokens = canon.split(/[\/+.-]+/g).filter(Boolean);
|
|
60
|
-
const trimmed = tokens[0] === 'application' ? tokens.slice(1) : tokens;
|
|
61
|
-
return pascalCase(trimmed.length ? trimmed : tokens) || 'Fmt';
|
|
62
|
-
}
|
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
import {canonicalizeMediaType} from './oas-contract.js';
|
|
2
1
|
import {collectInterfaceTypeNames, resolveInterfaceSchema} from './interface-schema.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import {assignTypeNames, resourceTypeName, selectModelSchemas} from './model-selection.js';
|
|
3
|
+
import {parseOpenApiSchemaName} from './oas-schema-name.js';
|
|
4
|
+
import {isObject, schemaNameFromRef} from './schema-utils.js';
|
|
5
5
|
import {createTypeResolver} from './type-resolver.js';
|
|
6
6
|
import {quoteKeyIfNeeded} from './utils.js';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* @typedef {{
|
|
10
|
-
* requiredMode?: 'all'|'spec',
|
|
11
|
-
* formats?: string[],
|
|
12
10
|
* includeSchemaNames?: Array<RegExp|string|((name: string) => boolean)>,
|
|
13
11
|
* excludeSchemaNames?: Array<RegExp|string|((name: string) => boolean)>,
|
|
14
12
|
* }} BuildOptions
|
|
@@ -28,12 +26,12 @@ import {quoteKeyIfNeeded} from './utils.js';
|
|
|
28
26
|
* }} ModelDefinition
|
|
29
27
|
*/
|
|
30
28
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
29
|
+
const JSON_LD_KEYWORDS = new Set(['@id', '@type', '@context']);
|
|
30
|
+
const HYDRA_SCHEMA = /^Hydra/i;
|
|
34
31
|
|
|
35
|
-
function createModelBuilder({spec, schemas,
|
|
32
|
+
function createModelBuilder({spec, schemas, schemaNames, typeNames}) {
|
|
36
33
|
const knownTypeNames = new Set(typeNames.values());
|
|
34
|
+
const interfaceTypeNames = collectInterfaceTypeNames(schemas, schemaNames, typeNames);
|
|
37
35
|
|
|
38
36
|
function importsFor(dependencies, currentTypeName) {
|
|
39
37
|
return [...dependencies]
|
|
@@ -42,7 +40,7 @@ function createModelBuilder({spec, schemas, typeNames, interfaceTypeNames, requi
|
|
|
42
40
|
}
|
|
43
41
|
|
|
44
42
|
function resolverFor(dependencies) {
|
|
45
|
-
return createTypeResolver({dependencies, typeNames,
|
|
43
|
+
return createTypeResolver({dependencies, typeNames, spec});
|
|
46
44
|
}
|
|
47
45
|
|
|
48
46
|
function buildAlias(raw, typeName) {
|
|
@@ -58,21 +56,28 @@ function createModelBuilder({spec, schemas, typeNames, interfaceTypeNames, requi
|
|
|
58
56
|
};
|
|
59
57
|
}
|
|
60
58
|
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
/** Whether the Hydra base schema of a representation requires `@id` and `@type`, like the runtime `Item`. */
|
|
60
|
+
function isHydraItem(raw) {
|
|
61
|
+
return (raw?.allOf ?? []).some((part) => {
|
|
62
|
+
const baseName = part?.$ref ? schemaNameFromRef(part.$ref) : undefined;
|
|
63
|
+
const required = baseName && HYDRA_SCHEMA.test(baseName) ? schemas[baseName]?.required ?? [] : [];
|
|
64
|
+
return required.includes('@id') && required.includes('@type');
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A JSON-LD item extends the runtime `Item`, which carries `@id`, `@type` and `@context`. */
|
|
69
|
+
function buildInterface(schema, typeName, extendsTypes, isJsonLd, extendsItem) {
|
|
63
70
|
const requiredProperties = new Set(schema.required ?? []);
|
|
64
71
|
const dependencies = new Set(extendsTypes);
|
|
65
72
|
const resolver = resolverFor(dependencies);
|
|
66
73
|
|
|
67
74
|
const props = Object.entries(schema.properties ?? {})
|
|
68
|
-
.filter(([propertyName]) => !isJsonLd || !
|
|
75
|
+
.filter(([propertyName]) => !isJsonLd || !JSON_LD_KEYWORDS.has(propertyName))
|
|
69
76
|
.map(([propertyName, propertySchema]) => ({
|
|
70
77
|
name: propertyName,
|
|
71
78
|
tsKey: quoteKeyIfNeeded(propertyName),
|
|
72
|
-
type:
|
|
73
|
-
|
|
74
|
-
: resolver.nullableTypeOf(propertySchema),
|
|
75
|
-
optional: requiredMode === 'spec' ? !requiredProperties.has(propertyName) : true,
|
|
79
|
+
type: resolver.typeOf(propertySchema),
|
|
80
|
+
optional: !requiredProperties.has(propertyName),
|
|
76
81
|
}));
|
|
77
82
|
|
|
78
83
|
return {
|
|
@@ -80,11 +85,11 @@ function createModelBuilder({spec, schemas, typeNames, interfaceTypeNames, requi
|
|
|
80
85
|
props,
|
|
81
86
|
imports: importsFor(dependencies, typeName),
|
|
82
87
|
extendsTypes,
|
|
83
|
-
extendsItem
|
|
88
|
+
extendsItem,
|
|
84
89
|
};
|
|
85
90
|
}
|
|
86
91
|
|
|
87
|
-
return function buildModel(schemaName
|
|
92
|
+
return function buildModel(schemaName) {
|
|
88
93
|
const raw = schemas[schemaName];
|
|
89
94
|
const typeName = typeNames.get(schemaName);
|
|
90
95
|
if (!typeName || raw === undefined) return undefined;
|
|
@@ -100,37 +105,41 @@ function createModelBuilder({spec, schemas, typeNames, interfaceTypeNames, requi
|
|
|
100
105
|
return buildAlias(raw, typeName);
|
|
101
106
|
}
|
|
102
107
|
|
|
103
|
-
|
|
108
|
+
const isJsonLd = parseOpenApiSchemaName(schemaName).baseVariant === 'jsonld';
|
|
109
|
+
return buildInterface(schema, typeName, extendsTypes, isJsonLd, isJsonLd && isHydraItem(raw));
|
|
104
110
|
};
|
|
105
111
|
}
|
|
106
112
|
|
|
113
|
+
/** TypeScript shape of a model, independent of its name and of the `Item` base. */
|
|
114
|
+
function signatureOf(model) {
|
|
115
|
+
if (!model) return '';
|
|
116
|
+
return JSON.stringify([
|
|
117
|
+
model.isAlias ? model.type : null,
|
|
118
|
+
model.extendsTypes,
|
|
119
|
+
model.props.map(({name, type, optional}) => [name, type, optional]),
|
|
120
|
+
]);
|
|
121
|
+
}
|
|
122
|
+
|
|
107
123
|
/**
|
|
108
|
-
* Builds TypeScript model definitions from OpenAPI `components.schemas`.
|
|
124
|
+
* Builds TypeScript model definitions from the OpenAPI `components.schemas`.
|
|
125
|
+
*
|
|
126
|
+
* API Platform describes one resource several times (`Book.jsonld`, `Book`, `Book.multipart`). Representations
|
|
127
|
+
* with the same shape produce a single model, built from the JSON-LD one when present.
|
|
128
|
+
*
|
|
109
129
|
* @param {any} spec
|
|
110
130
|
* @param {BuildOptions} [options]
|
|
111
131
|
* @returns {{models: ModelDefinition[]}}
|
|
112
132
|
*/
|
|
113
133
|
export function buildModelsFromOpenAPI(spec, options = {}) {
|
|
114
|
-
const
|
|
115
|
-
const {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
);
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
+
const {schemas, schemaNames} = selectModelSchemas(spec, options);
|
|
135
|
+
const builderFor = (typeNames) => createModelBuilder({spec, schemas, schemaNames, typeNames});
|
|
136
|
+
|
|
137
|
+
// Representations of a resource are compared with the resource name, so references do not tell them apart.
|
|
138
|
+
const buildWithResourceNames = builderFor(new Map(schemaNames.map((name) => [name, resourceTypeName(name)])));
|
|
139
|
+
const signatures = new Map(schemaNames.map((name) => [name, signatureOf(buildWithResourceNames(name))]));
|
|
140
|
+
|
|
141
|
+
const {typeNames, emitted} = assignTypeNames(schemaNames, signatures);
|
|
142
|
+
const buildModel = builderFor(typeNames);
|
|
134
143
|
|
|
135
|
-
return {models};
|
|
144
|
+
return {models: emitted.map(buildModel).filter(Boolean)};
|
|
136
145
|
}
|