@obsidiane/meridiane 2.0.13 → 3.0.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.
Files changed (58) hide show
  1. package/README.md +30 -68
  2. package/cli.js +1 -10
  3. package/package.json +1 -1
  4. package/src/application/bridge-workflow.js +23 -66
  5. package/src/application/generate-bridge.js +32 -36
  6. package/src/application/run-build.js +8 -30
  7. package/src/application/run-dev.js +8 -63
  8. package/src/application/run-generate.js +8 -47
  9. package/src/domain/bridge-options.js +10 -63
  10. package/src/generators/bridge/write-bridge-files.js +11 -29
  11. package/src/generators/models/interface-schema.js +3 -20
  12. package/src/generators/models/model-selection.js +67 -106
  13. package/src/generators/models/oas-contract.js +1 -108
  14. package/src/generators/models/oas-schema-name.js +1 -19
  15. package/src/generators/models/openapi-to-models.js +51 -42
  16. package/src/generators/models/schema-utils.js +0 -34
  17. package/src/generators/models/type-resolver.js +2 -20
  18. package/src/generators/models/utils.js +0 -11
  19. package/src/generators/models/write-model-files.js +5 -15
  20. package/src/infrastructure/dev-project.js +0 -33
  21. package/src/infrastructure/files.js +15 -0
  22. package/src/infrastructure/local-package-install.js +2 -2
  23. package/src/infrastructure/logger.js +0 -2
  24. package/templates/angular-bridge/README.md +29 -281
  25. package/templates/angular-bridge/package.json +2 -2
  26. package/templates/angular-bridge/src/lib/bridge/rest/http-request.options.ts +10 -19
  27. package/templates/angular-bridge/src/lib/bridge/sse/mercure-hub.ts +449 -0
  28. package/templates/angular-bridge/src/lib/bridge/sse/mercure-url.ts +28 -0
  29. package/templates/angular-bridge/src/lib/bridge/sse/resource-topic.ts +83 -0
  30. package/templates/angular-bridge/src/lib/bridge-options.ts +21 -17
  31. package/templates/angular-bridge/src/lib/bridge.types.ts +22 -33
  32. package/templates/angular-bridge/src/lib/facades/bridge.facade.ts +58 -134
  33. package/templates/angular-bridge/src/lib/facades/facade.factory.ts +25 -25
  34. package/templates/angular-bridge/src/lib/facades/facade.interface.ts +4 -9
  35. package/templates/angular-bridge/src/lib/facades/resource.facade.ts +38 -61
  36. package/templates/angular-bridge/src/lib/interceptors/bearer-auth.interceptor.ts +8 -12
  37. package/templates/angular-bridge/src/lib/interceptors/bridge-defaults.interceptor.ts +19 -11
  38. package/templates/angular-bridge/src/lib/interceptors/content-type.interceptor.ts +5 -17
  39. package/templates/angular-bridge/src/lib/interceptors/singleflight.interceptor.ts +28 -24
  40. package/templates/angular-bridge/src/lib/ports/realtime.port.ts +34 -53
  41. package/templates/angular-bridge/src/lib/ports/resource-repository.port.ts +31 -21
  42. package/templates/angular-bridge/src/lib/provide-bridge.ts +14 -38
  43. package/templates/angular-bridge/src/lib/tokens.ts +4 -15
  44. package/templates/angular-bridge/src/lib/utils/request-url.ts +4 -6
  45. package/templates/angular-bridge/src/lib/utils/url.ts +32 -9
  46. package/templates/angular-bridge/src/public-api.ts +0 -1
  47. package/templates/angular-bridge/tsconfig.lib.json +0 -3
  48. package/src/infrastructure/json.js +0 -71
  49. package/templates/angular-bridge/src/lib/bridge/rest/api-platform.adapter.ts +0 -82
  50. package/templates/angular-bridge/src/lib/bridge/sse/eventsource-wrapper.ts +0 -70
  51. package/templates/angular-bridge/src/lib/bridge/sse/mercure-connections.ts +0 -255
  52. package/templates/angular-bridge/src/lib/bridge/sse/mercure-payload.ts +0 -82
  53. package/templates/angular-bridge/src/lib/bridge/sse/mercure-topic.mapper.ts +0 -48
  54. package/templates/angular-bridge/src/lib/bridge/sse/mercure-url.builder.ts +0 -39
  55. package/templates/angular-bridge/src/lib/bridge/sse/mercure.adapter.ts +0 -358
  56. package/templates/angular-bridge/src/lib/bridge/sse/ref-count-topic.registry.ts +0 -36
  57. package/templates/angular-bridge/src/lib/bridge/sse/subscription-claims.ts +0 -76
  58. 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, writeJsonIfChanged } from '../../infrastructure/json.js';
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 fs.rm(projectDir, { recursive: true, force: true });
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 writeJsonIfChanged(packageJsonPath, packageJson);
26
+ await writeJson(packageJsonPath, packageJson);
43
27
 
44
- await appendProjectReadme(path.join(projectDir, 'README.md'), projectReadmePath, debug, log);
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 writeJsonIfChanged(ngPackagePath, ngPackage);
54
- log?.debug?.('ng-package.json dest overridden', { dest: ngPackage.dest });
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, debug, log) {
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
- if (debug) log?.debug?.('[meridiane] project README copied', { source: sourcePath });
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
- if (debug) log?.debug?.('[meridiane] project README appended', { source: sourcePath });
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, mergeAllOf, schemaNameFromRef} from './schema-utils.js';
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. Unsafe or conflicting compositions fall back to a type alias.
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
- 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
- }
9
+ const HYDRA_SCHEMA = /^Hydra/i;
37
10
 
38
- return formatsBySchema;
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
- 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));
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 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};
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
- function desiredTypeName(schemaName) {
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
- if (parsed.isMergePatch) return undefined;
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
- function assignTypeNames(schemaNames, preferredFormats, formatPriority) {
98
- const schemasByDesiredName = new Map();
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
- for (const schemaName of schemaNames) {
101
- const desiredName = desiredTypeName(schemaName);
102
- if (!desiredName) continue;
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
- const entries = schemasByDesiredName.get(desiredName) ?? [];
105
- entries.push({schemaName, preferredFormat: preferredFormats.get(schemaName)});
106
- schemasByDesiredName.set(desiredName, entries);
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 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
- });
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 {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']);
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
- const basePath = baseRaw;
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 {DEFAULT_MODEL_FORMAT, selectModelSchemas} from './model-selection.js';
4
- import {isObject} from './schema-utils.js';
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
- function normalizeRequiredMode(value) {
32
- return String(value ?? '').trim().toLowerCase() === 'spec' ? 'spec' : 'all';
33
- }
29
+ const JSON_LD_KEYWORDS = new Set(['@id', '@type', '@context']);
30
+ const HYDRA_SCHEMA = /^Hydra/i;
34
31
 
35
- function createModelBuilder({spec, schemas, typeNames, interfaceTypeNames, requiredMode}) {
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, requiredMode, spec});
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
- function buildInterface(schema, typeName, extendsTypes, preferredFormat) {
62
- const isJsonLd = canonicalizeMediaType(preferredFormat) === DEFAULT_MODEL_FORMAT;
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 || !['@id', '@type', '@context'].includes(propertyName))
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: requiredMode === 'spec'
73
- ? resolver.typeOf(propertySchema)
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: isJsonLd,
88
+ extendsItem,
84
89
  };
85
90
  }
86
91
 
87
- return function buildModel(schemaName, preferredFormat) {
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
- return buildInterface(schema, typeName, extendsTypes, preferredFormat);
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 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
+ 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
  }