@obsidiane/meridiane 0.0.13 → 1.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
CHANGED
|
@@ -31,7 +31,11 @@ Options :
|
|
|
31
31
|
- `--debug` : active des logs supplémentaires (CLI).
|
|
32
32
|
|
|
33
33
|
Bonnes pratiques :
|
|
34
|
-
- utiliser `--preset=native`
|
|
34
|
+
- utiliser `--preset=native` pour un mode “contract-driven” (LD+JSON) :
|
|
35
|
+
- modèles générés uniquement s’ils sont utilisés par les endpoints
|
|
36
|
+
- groups conservés
|
|
37
|
+
- pas de modèles `*.jsonMergePatch` (PATCH = `Partial<...>`)
|
|
38
|
+
- noms normalisés (pas de suffixe `.jsonld`)
|
|
35
39
|
- utiliser `--no-models` si vous voulez uniquement le runtime (pas besoin de `--spec`)
|
|
36
40
|
- laisser le registry à la CI (`.npmrc`, variables d’environnement, `npm publish --registry …`)
|
|
37
41
|
|
package/package.json
CHANGED
|
@@ -50,6 +50,146 @@ function buildNameMap(schemaNames, schemas, preset) {
|
|
|
50
50
|
return nameMap;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
function getContentSchema(content, wantedLower) {
|
|
54
|
+
if (!content || typeof content !== 'object') return undefined;
|
|
55
|
+
for (const [k, v] of Object.entries(content)) {
|
|
56
|
+
if (String(k).toLowerCase() === wantedLower) return v?.schema;
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function is2xxStatus(code) {
|
|
62
|
+
const s = String(code || '');
|
|
63
|
+
return /^[0-9]{3}$/.test(s) && s.startsWith('2');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function collectRefsFromSchema(schema, out) {
|
|
67
|
+
const seen = new Set();
|
|
68
|
+
|
|
69
|
+
function walk(node) {
|
|
70
|
+
if (!node || typeof node !== 'object') return;
|
|
71
|
+
if (seen.has(node)) return;
|
|
72
|
+
seen.add(node);
|
|
73
|
+
|
|
74
|
+
if (node.$ref && typeof node.$ref === 'string') {
|
|
75
|
+
out.add(schemaNameFromRef(node.$ref));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (Array.isArray(node)) {
|
|
80
|
+
for (const it of node) walk(it);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Common JSON schema constructs (plus a generic fallback)
|
|
85
|
+
if (node.allOf) walk(node.allOf);
|
|
86
|
+
if (node.oneOf) walk(node.oneOf);
|
|
87
|
+
if (node.anyOf) walk(node.anyOf);
|
|
88
|
+
if (node.items) walk(node.items);
|
|
89
|
+
if (node.properties) walk(node.properties);
|
|
90
|
+
if (node.additionalProperties) walk(node.additionalProperties);
|
|
91
|
+
|
|
92
|
+
for (const v of Object.values(node)) walk(v);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
walk(schema);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function listNativeRootsFromPaths(spec, { mediaType = 'application/ld+json' } = {}) {
|
|
99
|
+
const wantedLower = String(mediaType).toLowerCase();
|
|
100
|
+
const roots = [];
|
|
101
|
+
|
|
102
|
+
const paths = spec?.paths;
|
|
103
|
+
if (!paths || typeof paths !== 'object') return roots;
|
|
104
|
+
|
|
105
|
+
for (const pathItem of Object.values(paths)) {
|
|
106
|
+
if (!pathItem || typeof pathItem !== 'object') continue;
|
|
107
|
+
for (const op of Object.values(pathItem)) {
|
|
108
|
+
if (!op || typeof op !== 'object') continue;
|
|
109
|
+
|
|
110
|
+
// requestBody (LD+JSON only)
|
|
111
|
+
const rbSchema = getContentSchema(op.requestBody?.content, wantedLower);
|
|
112
|
+
if (rbSchema) roots.push(rbSchema);
|
|
113
|
+
|
|
114
|
+
// responses 2xx (LD+JSON only)
|
|
115
|
+
const responses = op.responses;
|
|
116
|
+
if (!responses || typeof responses !== 'object') continue;
|
|
117
|
+
for (const [code, resp] of Object.entries(responses)) {
|
|
118
|
+
if (!is2xxStatus(code)) continue;
|
|
119
|
+
const schema = getContentSchema(resp?.content, wantedLower);
|
|
120
|
+
if (schema) roots.push(schema);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return roots;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function parseOpenApiSchemaName(originalName) {
|
|
129
|
+
const raw = String(originalName || '');
|
|
130
|
+
const i = raw.indexOf('-');
|
|
131
|
+
let baseRaw = i >= 0 ? raw.slice(0, i) : raw;
|
|
132
|
+
let groupRaw = i >= 0 ? raw.slice(i + 1) : '';
|
|
133
|
+
|
|
134
|
+
let baseVariant = 'none';
|
|
135
|
+
if (/\.jsonld$/i.test(baseRaw)) {
|
|
136
|
+
baseVariant = 'jsonld';
|
|
137
|
+
baseRaw = baseRaw.replace(/\.jsonld$/i, '');
|
|
138
|
+
} else if (/\.multipart$/i.test(baseRaw)) {
|
|
139
|
+
baseVariant = 'multipart';
|
|
140
|
+
baseRaw = baseRaw.replace(/\.multipart$/i, '');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let isMergePatch = false;
|
|
144
|
+
if (/\.jsonmergepatch$/i.test(baseRaw)) {
|
|
145
|
+
isMergePatch = true;
|
|
146
|
+
baseRaw = baseRaw.replace(/\.jsonmergepatch$/i, '');
|
|
147
|
+
}
|
|
148
|
+
if (/\.jsonmergepatch$/i.test(groupRaw)) {
|
|
149
|
+
isMergePatch = true;
|
|
150
|
+
groupRaw = groupRaw.replace(/\.jsonmergepatch$/i, '');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
originalName: raw,
|
|
155
|
+
basePath: baseRaw,
|
|
156
|
+
groupPath: groupRaw,
|
|
157
|
+
baseVariant,
|
|
158
|
+
isMergePatch,
|
|
159
|
+
key: `${baseRaw.toLowerCase()}|${groupRaw.toLowerCase()}`,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function pascalizeTokens(tokens) {
|
|
164
|
+
return tokens
|
|
165
|
+
.filter(Boolean)
|
|
166
|
+
.map((t) => String(t).replace(/^[^A-Za-z0-9]+|[^A-Za-z0-9]+$/g, ''))
|
|
167
|
+
.filter(Boolean)
|
|
168
|
+
.map((t) => t.charAt(0).toUpperCase() + t.slice(1))
|
|
169
|
+
.join('');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function baseTsNameFromBasePath(basePath) {
|
|
173
|
+
const parts = String(basePath || '').split('.').filter(Boolean);
|
|
174
|
+
if (parts.length === 0) return 'Model';
|
|
175
|
+
const a = pascalizeTokens([parts[0]]);
|
|
176
|
+
if (parts.length === 1) return a;
|
|
177
|
+
const b = pascalizeTokens([parts[1]]);
|
|
178
|
+
if (parts.length === 2 && b.toLowerCase().startsWith(a.toLowerCase())) return b;
|
|
179
|
+
return parts.map((p) => pascalizeTokens([p])).join('');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function groupTsNameFromGroupPath(groupPath) {
|
|
183
|
+
const tokens = String(groupPath || '').split(/[._:]+/g).filter(Boolean);
|
|
184
|
+
return pascalizeTokens(tokens);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function stableUnion(types) {
|
|
188
|
+
const uniq = [...new Set(types.filter(Boolean))];
|
|
189
|
+
uniq.sort((a, b) => a.localeCompare(b));
|
|
190
|
+
return uniq.join(' | ') || 'any';
|
|
191
|
+
}
|
|
192
|
+
|
|
53
193
|
/**
|
|
54
194
|
* Construit les définitions de modèles à partir d’une spec OpenAPI (components.schemas)
|
|
55
195
|
* @param {any} spec
|
|
@@ -59,77 +199,249 @@ function buildNameMap(schemaNames, schemas, preset) {
|
|
|
59
199
|
export function buildModelsFromOpenAPI(spec, options) {
|
|
60
200
|
const opts = { requiredMode: 'all-optional', ...(options || {}) };
|
|
61
201
|
const schemas = spec?.components?.schemas || {};
|
|
62
|
-
const schemaNames = applySchemaNameFilters(filterSchemaNames(schemas, opts), opts);
|
|
63
202
|
const preset = opts.preset ?? 'all';
|
|
64
|
-
const nameMap = buildNameMap(schemaNames, schemas, preset);
|
|
65
|
-
const allSanitizedNames = new Set([...nameMap.values()]);
|
|
66
203
|
|
|
67
204
|
/** @type {ModelDefinition[]} */
|
|
68
205
|
const models = [];
|
|
69
206
|
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
|
|
207
|
+
if (preset !== 'native') {
|
|
208
|
+
const schemaNames = applySchemaNameFilters(filterSchemaNames(schemas, opts), opts);
|
|
209
|
+
const nameMap = buildNameMap(schemaNames, schemas, preset);
|
|
210
|
+
const allSanitizedNames = new Set([...nameMap.values()]);
|
|
211
|
+
|
|
212
|
+
for (const originalName of schemaNames) {
|
|
213
|
+
const raw = schemas[originalName];
|
|
214
|
+
const sanitized = nameMap.get(originalName);
|
|
215
|
+
if (!sanitized) continue;
|
|
216
|
+
|
|
217
|
+
/** @type {string[]} */
|
|
218
|
+
const extendsTypes = [];
|
|
74
219
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
continue;
|
|
220
|
+
let effective = raw;
|
|
221
|
+
if (raw?.allOf) {
|
|
222
|
+
const merged = mergeAllOf(raw.allOf);
|
|
223
|
+
if (merged) {
|
|
224
|
+
effective = merged;
|
|
225
|
+
} else {
|
|
226
|
+
// Non-flattenable allOf (refs, mixed forms): keep inline object props and model refs as extends.
|
|
227
|
+
const props = {};
|
|
228
|
+
const required = new Set();
|
|
229
|
+
const ext = new Set();
|
|
230
|
+
|
|
231
|
+
for (const part of raw.allOf) {
|
|
232
|
+
if (part?.$ref) {
|
|
233
|
+
const orig = schemaNameFromRef(part.$ref);
|
|
234
|
+
const refName = orig ? nameMap.get(orig) : undefined;
|
|
235
|
+
if (refName && refName !== sanitized && allSanitizedNames.has(refName)) ext.add(refName);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const isObj = part?.type === 'object' || isObject(part?.properties);
|
|
239
|
+
if (!isObj) continue;
|
|
240
|
+
Object.assign(props, part.properties || {});
|
|
241
|
+
for (const r of part.required || []) required.add(r);
|
|
95
242
|
}
|
|
96
|
-
const isObj = part?.type === 'object' || isObject(part?.properties);
|
|
97
|
-
if (!isObj) continue;
|
|
98
|
-
Object.assign(props, part.properties || {});
|
|
99
|
-
for (const r of part.required || []) required.add(r);
|
|
100
|
-
}
|
|
101
243
|
|
|
102
|
-
|
|
103
|
-
|
|
244
|
+
extendsTypes.push(...[...ext].sort((a, b) => a.localeCompare(b)));
|
|
245
|
+
effective = { type: 'object', properties: props, required: [...required] };
|
|
246
|
+
}
|
|
104
247
|
}
|
|
248
|
+
|
|
249
|
+
const isObjectLike = effective?.type === 'object' || isObject(effective?.properties) || extendsTypes.length > 0;
|
|
250
|
+
if (!isObjectLike) continue;
|
|
251
|
+
|
|
252
|
+
const required = new Set(effective.required || []);
|
|
253
|
+
const deps = new Set();
|
|
254
|
+
|
|
255
|
+
for (const e of extendsTypes) deps.add(e);
|
|
256
|
+
|
|
257
|
+
const props = Object.entries(effective.properties || {}).map(([propName, prop]) => {
|
|
258
|
+
const baseType = tsTypeOf(prop, deps, nameMap, { requiredMode: opts.requiredMode ?? 'all-optional', forceNullable: false });
|
|
259
|
+
const type = withNullable(baseType, prop);
|
|
260
|
+
return {
|
|
261
|
+
name: propName,
|
|
262
|
+
tsKey: quoteKeyIfNeeded(propName),
|
|
263
|
+
type,
|
|
264
|
+
optional: opts.requiredMode === 'spec' ? !required.has(propName) : true,
|
|
265
|
+
};
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const imports = [...deps]
|
|
269
|
+
.filter((n) => n !== sanitized)
|
|
270
|
+
.filter((n) => allSanitizedNames.has(n))
|
|
271
|
+
.sort((a, b) => a.localeCompare(b));
|
|
272
|
+
|
|
273
|
+
models.push({name: sanitized, props, imports, extendsTypes});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return { models };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// preset=native (v2): contract-driven (paths), LD+JSON, groups preserved, jsonld/non-jsonld merged.
|
|
280
|
+
const roots = listNativeRootsFromPaths(spec, { mediaType: 'application/ld+json' });
|
|
281
|
+
const initialRefs = new Set();
|
|
282
|
+
for (const r of roots) collectRefsFromSchema(r, initialRefs);
|
|
283
|
+
|
|
284
|
+
const usedSchemaNames = new Set();
|
|
285
|
+
const queue = [...initialRefs];
|
|
286
|
+
while (queue.length) {
|
|
287
|
+
const name = queue.pop();
|
|
288
|
+
if (!name || usedSchemaNames.has(name)) continue;
|
|
289
|
+
usedSchemaNames.add(name);
|
|
290
|
+
const schema = schemas[name];
|
|
291
|
+
if (!schema) continue;
|
|
292
|
+
const refs = new Set();
|
|
293
|
+
collectRefsFromSchema(schema, refs);
|
|
294
|
+
for (const ref of refs) if (ref && !usedSchemaNames.has(ref)) queue.push(ref);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Group and merge by (basePath, groupPath), ignoring variants.
|
|
298
|
+
const buckets = new Map();
|
|
299
|
+
for (const originalName of [...usedSchemaNames].sort((a, b) => a.localeCompare(b))) {
|
|
300
|
+
if (!originalName) continue;
|
|
301
|
+
if (/^Hydra/i.test(originalName)) continue; // technical
|
|
302
|
+
|
|
303
|
+
const parsed = parseOpenApiSchemaName(originalName);
|
|
304
|
+
if (!parsed.basePath) continue;
|
|
305
|
+
|
|
306
|
+
// User include/exclude filters (apply to canonical name and to raw variants)
|
|
307
|
+
const canonical = parsed.groupPath ? `${parsed.basePath}-${parsed.groupPath}` : parsed.basePath;
|
|
308
|
+
const fulls = [originalName, canonical];
|
|
309
|
+
const include = Array.isArray(opts.includeSchemaNames) ? opts.includeSchemaNames : [];
|
|
310
|
+
const exclude = Array.isArray(opts.excludeSchemaNames) ? opts.excludeSchemaNames : [];
|
|
311
|
+
|
|
312
|
+
const matches = (val, rule) => {
|
|
313
|
+
if (rule instanceof RegExp) return rule.test(val);
|
|
314
|
+
if (typeof rule === 'function') return !!rule(val);
|
|
315
|
+
if (typeof rule === 'string' && rule.length) return val.includes(rule);
|
|
316
|
+
return false;
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const hasInclude = include.length > 0;
|
|
320
|
+
const included = !hasInclude || include.some((r) => fulls.some((v) => matches(v, r)));
|
|
321
|
+
const excluded = exclude.some((r) => fulls.some((v) => matches(v, r)));
|
|
322
|
+
if (!included || excluded) continue;
|
|
323
|
+
|
|
324
|
+
const bucket = buckets.get(parsed.key) || { basePath: parsed.basePath, groupPath: parsed.groupPath, entries: [] };
|
|
325
|
+
bucket.entries.push(parsed);
|
|
326
|
+
buckets.set(parsed.key, bucket);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Stable bucket ordering
|
|
330
|
+
const bucketList = [...buckets.values()].sort((a, b) => {
|
|
331
|
+
const ka = `${a.basePath}-${a.groupPath}`.toLowerCase();
|
|
332
|
+
const kb = `${b.basePath}-${b.groupPath}`.toLowerCase();
|
|
333
|
+
return ka.localeCompare(kb);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// Compute TS names (unique) and build originalName -> tsName map (all variants in a bucket).
|
|
337
|
+
const usedTsNames = new Set();
|
|
338
|
+
const nameMap = new Map();
|
|
339
|
+
for (const b of bucketList) {
|
|
340
|
+
const baseTs = baseTsNameFromBasePath(b.basePath);
|
|
341
|
+
const groupTs = groupTsNameFromGroupPath(b.groupPath);
|
|
342
|
+
const desired = `${baseTs}${groupTs}` || 'Model';
|
|
343
|
+
const tsName = sanitizeTypeName(desired, usedTsNames);
|
|
344
|
+
b.tsName = tsName;
|
|
345
|
+
for (const e of b.entries) {
|
|
346
|
+
nameMap.set(e.originalName, tsName);
|
|
105
347
|
}
|
|
348
|
+
}
|
|
349
|
+
const allSanitizedNames = new Set(bucketList.map((b) => b.tsName));
|
|
106
350
|
|
|
107
|
-
|
|
108
|
-
|
|
351
|
+
// Generate merged models per bucket.
|
|
352
|
+
for (const b of bucketList) {
|
|
353
|
+
const sanitized = b.tsName;
|
|
354
|
+
if (!sanitized) continue;
|
|
109
355
|
|
|
110
|
-
const required = new Set(effective.required || []);
|
|
111
356
|
const deps = new Set();
|
|
357
|
+
const extendsSet = new Set();
|
|
358
|
+
const propTypes = new Map(); // propName -> Set<tsType>
|
|
112
359
|
|
|
113
|
-
|
|
360
|
+
// Choose candidates: jsonld + base (no multipart, no merge-patch)
|
|
361
|
+
const candidates = b.entries
|
|
362
|
+
.filter((e) => !e.isMergePatch)
|
|
363
|
+
.filter((e) => e.baseVariant !== 'multipart')
|
|
364
|
+
.sort((a, b2) => {
|
|
365
|
+
const score = (e) => (e.baseVariant === 'jsonld' ? 2 : 1);
|
|
366
|
+
const sa = score(a), sb = score(b2);
|
|
367
|
+
if (sb !== sa) return sb - sa;
|
|
368
|
+
return a.originalName.localeCompare(b2.originalName);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
for (const e of candidates) {
|
|
372
|
+
const raw = schemas[e.originalName];
|
|
373
|
+
if (!raw) continue;
|
|
374
|
+
|
|
375
|
+
let effective = raw;
|
|
376
|
+
/** @type {string[]} */
|
|
377
|
+
const extendsTypes = [];
|
|
378
|
+
|
|
379
|
+
if (raw?.allOf) {
|
|
380
|
+
const merged = mergeAllOf(raw.allOf);
|
|
381
|
+
if (merged) {
|
|
382
|
+
effective = merged;
|
|
383
|
+
} else {
|
|
384
|
+
const props = {};
|
|
385
|
+
const ext = new Set();
|
|
386
|
+
for (const part of raw.allOf) {
|
|
387
|
+
if (part?.$ref) {
|
|
388
|
+
const orig = schemaNameFromRef(part.$ref);
|
|
389
|
+
const refName = orig ? nameMap.get(orig) || orig : undefined;
|
|
390
|
+
if (refName && refName !== sanitized && allSanitizedNames.has(refName)) ext.add(refName);
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
const isObj = part?.type === 'object' || isObject(part?.properties);
|
|
394
|
+
if (!isObj) continue;
|
|
395
|
+
Object.assign(props, part.properties || {});
|
|
396
|
+
}
|
|
397
|
+
extendsTypes.push(...[...ext].sort((a, b3) => a.localeCompare(b3)));
|
|
398
|
+
effective = { type: 'object', properties: props };
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const isObjectLike = effective?.type === 'object' || isObject(effective?.properties) || extendsTypes.length > 0;
|
|
403
|
+
if (!isObjectLike) continue;
|
|
404
|
+
|
|
405
|
+
for (const ex of extendsTypes) extendsSet.add(ex);
|
|
114
406
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
optional: opts.requiredMode === 'spec' ? !required.has(propName) : true,
|
|
123
|
-
};
|
|
124
|
-
});
|
|
407
|
+
for (const [propName, prop] of Object.entries(effective.properties || {})) {
|
|
408
|
+
const baseType = tsTypeOf(prop, deps, nameMap, { requiredMode: 'all-optional', forceNullable: true });
|
|
409
|
+
const existing = propTypes.get(propName) || new Set();
|
|
410
|
+
existing.add(baseType);
|
|
411
|
+
propTypes.set(propName, existing);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
125
414
|
|
|
415
|
+
const extendsTypes = [...extendsSet]
|
|
416
|
+
.filter((n) => n !== sanitized)
|
|
417
|
+
.filter((n) => allSanitizedNames.has(n))
|
|
418
|
+
.sort((a, b2) => a.localeCompare(b2));
|
|
419
|
+
|
|
420
|
+
// Imports are any referenced model types (+ extends), excluding self.
|
|
421
|
+
for (const e of extendsTypes) deps.add(e);
|
|
126
422
|
const imports = [...deps]
|
|
127
423
|
.filter((n) => n !== sanitized)
|
|
128
424
|
.filter((n) => allSanitizedNames.has(n))
|
|
129
|
-
.sort((a,
|
|
425
|
+
.sort((a, b2) => a.localeCompare(b2));
|
|
426
|
+
|
|
427
|
+
const props = [...propTypes.keys()]
|
|
428
|
+
.sort((a, b2) => a.localeCompare(b2))
|
|
429
|
+
.map((propName) => {
|
|
430
|
+
const union = stableUnion([...propTypes.get(propName)]);
|
|
431
|
+
const type = /\|\s*null\b/.test(union) ? union : `${union} | null`;
|
|
432
|
+
return {
|
|
433
|
+
name: propName,
|
|
434
|
+
tsKey: quoteKeyIfNeeded(propName),
|
|
435
|
+
type,
|
|
436
|
+
optional: true,
|
|
437
|
+
};
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// Skip empty models (no props + no extends): avoid polluting exports.
|
|
441
|
+
if (props.length === 0 && extendsTypes.length === 0) continue;
|
|
130
442
|
|
|
131
|
-
models.push({name: sanitized, props, imports, extendsTypes});
|
|
443
|
+
models.push({ name: sanitized, props, imports, extendsTypes });
|
|
132
444
|
}
|
|
133
445
|
|
|
134
|
-
return {models};
|
|
446
|
+
return { models };
|
|
135
447
|
}
|
|
@@ -112,6 +112,9 @@ export function applySchemaNameFilters(schemaNames, cfg) {
|
|
|
112
112
|
if (preset === 'native') {
|
|
113
113
|
out = out
|
|
114
114
|
.filter((n) => !/^Hydra/i.test(n))
|
|
115
|
+
// API Platform adds a technical "Multipart" schema for multipart/form-data request bodies (file uploads).
|
|
116
|
+
// It's not a resource model and tends to pollute the generated "native" model list.
|
|
117
|
+
.filter((n) => !/^Multipart(?:$|[.-])/i.test(n))
|
|
115
118
|
.filter((n) => !/jsonMergePatch/i.test(n))
|
|
116
119
|
.filter((n) => !/\.jsonld\b/i.test(n))
|
|
117
120
|
.filter((n) => !/\.jsonapi\b/i.test(n));
|
|
@@ -16,10 +16,13 @@ export function withNullable(ts, schema) {
|
|
|
16
16
|
* @param {any} schema
|
|
17
17
|
* @param {Set<string>} deps
|
|
18
18
|
* @param {Map<string,string>} nameMap
|
|
19
|
+
* @param {{ requiredMode?: 'all-optional'|'spec', forceNullable?: boolean }} [cfg]
|
|
19
20
|
* @returns {string}
|
|
20
21
|
*/
|
|
21
|
-
export function tsTypeOf(schema, deps, nameMap) {
|
|
22
|
+
export function tsTypeOf(schema, deps, nameMap, cfg) {
|
|
22
23
|
if (!schema) return 'any';
|
|
24
|
+
const requiredMode = cfg?.requiredMode ?? 'all-optional';
|
|
25
|
+
const forceNullable = !!cfg?.forceNullable;
|
|
23
26
|
|
|
24
27
|
if (schema.$ref) {
|
|
25
28
|
const orig = schemaNameFromRef(schema.$ref);
|
|
@@ -49,9 +52,14 @@ export function tsTypeOf(schema, deps, nameMap) {
|
|
|
49
52
|
else if (t === 'string') parts.add('string');
|
|
50
53
|
else if (t === 'integer' || t === 'number') parts.add('number');
|
|
51
54
|
else if (t === 'boolean') parts.add('boolean');
|
|
52
|
-
else if (t === 'array') parts.add(tsTypeOf({ type: 'array', items: schema.items }, deps, nameMap));
|
|
55
|
+
else if (t === 'array') parts.add(tsTypeOf({ type: 'array', items: schema.items }, deps, nameMap, cfg));
|
|
53
56
|
else if (t === 'object') {
|
|
54
|
-
const objTs = tsTypeOf(
|
|
57
|
+
const objTs = tsTypeOf(
|
|
58
|
+
{ type: 'object', properties: schema.properties, required: schema.required, additionalProperties: schema.additionalProperties },
|
|
59
|
+
deps,
|
|
60
|
+
nameMap,
|
|
61
|
+
cfg
|
|
62
|
+
);
|
|
55
63
|
parts.add(objTs);
|
|
56
64
|
} else parts.add('any');
|
|
57
65
|
}
|
|
@@ -60,13 +68,13 @@ export function tsTypeOf(schema, deps, nameMap) {
|
|
|
60
68
|
|
|
61
69
|
if (schema.allOf) {
|
|
62
70
|
// Aplatir au niveau buildModelsFromOpenAPI; ici on combine si nécessaire
|
|
63
|
-
return schema.allOf.map((s) => tsTypeOf(s, deps, nameMap)).join(' & ');
|
|
71
|
+
return schema.allOf.map((s) => tsTypeOf(s, deps, nameMap, cfg)).join(' & ');
|
|
64
72
|
}
|
|
65
|
-
if (schema.oneOf) return schema.oneOf.map((s) => tsTypeOf(s, deps, nameMap)).join(' | ');
|
|
66
|
-
if (schema.anyOf) return schema.anyOf.map((s) => tsTypeOf(s, deps, nameMap)).join(' | ');
|
|
73
|
+
if (schema.oneOf) return schema.oneOf.map((s) => tsTypeOf(s, deps, nameMap, cfg)).join(' | ');
|
|
74
|
+
if (schema.anyOf) return schema.anyOf.map((s) => tsTypeOf(s, deps, nameMap, cfg)).join(' | ');
|
|
67
75
|
|
|
68
76
|
if (schema.type === 'array') {
|
|
69
|
-
const it = tsTypeOf(schema.items || {}, deps, nameMap);
|
|
77
|
+
const it = tsTypeOf(schema.items || {}, deps, nameMap, cfg);
|
|
70
78
|
return `${it}[]`;
|
|
71
79
|
}
|
|
72
80
|
|
|
@@ -74,14 +82,15 @@ export function tsTypeOf(schema, deps, nameMap) {
|
|
|
74
82
|
const props = schema.properties || {};
|
|
75
83
|
const entries = Object.entries(props);
|
|
76
84
|
const inline = entries.map(([k, v]) => {
|
|
77
|
-
const
|
|
78
|
-
const opt = (schema.required || []).includes(k) ? '' : '?';
|
|
79
|
-
const
|
|
85
|
+
const t0 = tsTypeOf(v, deps, nameMap, cfg);
|
|
86
|
+
const opt = requiredMode === 'spec' ? ((schema.required || []).includes(k) ? '' : '?') : '?';
|
|
87
|
+
const wantsNull = forceNullable || v?.nullable;
|
|
88
|
+
const nullable = wantsNull && !/\|\s*null\b/.test(t0) ? ' | null' : '';
|
|
80
89
|
const key = quoteKeyIfNeeded(k);
|
|
81
|
-
return `${key}${opt}: ${
|
|
90
|
+
return `${key}${opt}: ${t0}${nullable};`;
|
|
82
91
|
});
|
|
83
92
|
if (schema.additionalProperties) {
|
|
84
|
-
const at = tsTypeOf(schema.additionalProperties, deps, nameMap);
|
|
93
|
+
const at = tsTypeOf(schema.additionalProperties, deps, nameMap, cfg);
|
|
85
94
|
inline.push(`[key: string]: ${at};`);
|
|
86
95
|
}
|
|
87
96
|
return `{ ${inline.join(' ')} }`;
|
|
@@ -93,4 +102,3 @@ export function tsTypeOf(schema, deps, nameMap) {
|
|
|
93
102
|
|
|
94
103
|
return 'any';
|
|
95
104
|
}
|
|
96
|
-
|