@obsidiane/meridiane 0.0.6 → 0.0.10

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 (47) hide show
  1. package/README.md +19 -31
  2. package/cli.js +64 -44
  3. package/package.json +3 -7
  4. package/templates/_lib_template/package.json +2 -1
  5. package/templates/_lib_template/src/lib/bridge/rest/api-platform.adapter.ts +48 -47
  6. package/templates/_lib_template/src/lib/bridge/rest/query-builder.ts +29 -29
  7. package/templates/_lib_template/src/lib/bridge/sse/eventsource-wrapper.ts +50 -54
  8. package/templates/_lib_template/src/lib/bridge/sse/mercure-topic.mapper.ts +48 -0
  9. package/templates/_lib_template/src/lib/bridge/sse/mercure-url.builder.ts +7 -7
  10. package/templates/_lib_template/src/lib/bridge/sse/mercure.adapter.ts +104 -49
  11. package/templates/_lib_template/src/lib/bridge/sse/ref-count-topic.registry.ts +23 -21
  12. package/templates/_lib_template/src/lib/bridge.types.ts +33 -0
  13. package/templates/_lib_template/src/lib/facades/bridge.facade.ts +116 -0
  14. package/templates/_lib_template/src/lib/facades/facade.factory.ts +21 -19
  15. package/templates/_lib_template/src/lib/facades/facade.interface.ts +13 -18
  16. package/templates/_lib_template/src/lib/facades/resource.facade.ts +42 -56
  17. package/templates/_lib_template/src/lib/interceptors/bridge-debug.interceptor.ts +32 -0
  18. package/templates/_lib_template/src/lib/interceptors/bridge-defaults.interceptor.ts +53 -0
  19. package/templates/_lib_template/src/lib/interceptors/content-type.interceptor.ts +36 -37
  20. package/templates/_lib_template/src/lib/ports/realtime.port.ts +5 -3
  21. package/templates/_lib_template/src/lib/ports/resource-repository.port.ts +14 -15
  22. package/templates/_lib_template/src/lib/provide-bridge.ts +122 -19
  23. package/templates/_lib_template/src/lib/tokens.ts +14 -1
  24. package/templates/_lib_template/src/lib/utils/url.ts +15 -0
  25. package/templates/_lib_template/src/models/index.ts +0 -1
  26. package/templates/_lib_template/src/public-api.ts +3 -2
  27. package/tools/build.js +104 -0
  28. package/tools/core/generate.js +152 -0
  29. package/tools/core/json.js +71 -0
  30. package/tools/core/lib-name.js +15 -0
  31. package/tools/core/logger.js +51 -0
  32. package/tools/core/paths.js +17 -0
  33. package/tools/core/spec.js +59 -0
  34. package/tools/core/standalone.js +117 -0
  35. package/tools/dev.js +218 -0
  36. package/tools/generator/models/handlebars.js +8 -2
  37. package/tools/generator/models/naming.js +21 -15
  38. package/tools/generator/models/openapi-to-models.js +75 -38
  39. package/tools/generator/models/schema-utils.js +84 -66
  40. package/tools/generator/models/templates/model.hbs +3 -3
  41. package/tools/generator/models/utils.js +1 -2
  42. package/models.config.example.js +0 -24
  43. package/templates/_lib_template/src/lib/bridge/credentials.policy.ts +0 -9
  44. package/templates/_lib_template/src/lib/utils/utils.ts +0 -21
  45. package/templates/_lib_template/tsconfig.spec.json +0 -14
  46. package/tools/generate-lib.js +0 -136
  47. package/tools/generate-models.js +0 -121
package/tools/dev.js ADDED
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env node
2
+ import process from 'node:process';
3
+ import fs from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { spawn } from 'node:child_process';
6
+
7
+ import { deriveLibName } from './core/lib-name.js';
8
+ import { normalizePreset, splitListArgs } from './core/paths.js';
9
+ import { readOpenApiSpec } from './core/spec.js';
10
+ import { createLogger } from './core/logger.js';
11
+ import { generateBridgeWorkspace } from './core/generate.js';
12
+ import { ensureStandaloneWorkspace, ensureToolchainInstalled } from './core/standalone.js';
13
+
14
+ function run(cmd, args, { cwd, env } = {}) {
15
+ return new Promise((resolve) => {
16
+ const child = spawn(cmd, args, {
17
+ stdio: 'inherit',
18
+ cwd: cwd ?? process.cwd(),
19
+ env: env ?? process.env,
20
+ });
21
+ child.on('exit', (code) => resolve(code ?? 0));
22
+ });
23
+ }
24
+
25
+ async function pathIsFile(p) {
26
+ try {
27
+ const st = await fs.stat(p);
28
+ return st.isFile();
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ async function readPackageJsonName(dir) {
35
+ try {
36
+ const raw = await fs.readFile(path.join(dir, 'package.json'), 'utf8');
37
+ const parsed = JSON.parse(raw);
38
+ return typeof parsed?.name === 'string' ? parsed.name : undefined;
39
+ } catch {
40
+ return undefined;
41
+ }
42
+ }
43
+
44
+ async function resolveDevAppRoot(startCwd) {
45
+ if (await pathIsFile(path.join(startCwd, 'package.json'))) return startCwd;
46
+ throw new Error('No package.json found. Run `meridiane dev` from your Angular app root.');
47
+ }
48
+
49
+ async function isMeridianeSandboxApp(appRoot) {
50
+ const name = await readPackageJsonName(appRoot);
51
+ return name === '@obsidiane/meridiane-sandbox';
52
+ }
53
+
54
+ async function resolveSandboxAppRoot(startCwd) {
55
+ // Allow running from apps/sandbox directly
56
+ if (await isMeridianeSandboxApp(startCwd)) return startCwd;
57
+
58
+ // Allow running from this repo root (monorepo) and target apps/sandbox
59
+ const sandboxApp = path.join(startCwd, 'apps', 'sandbox');
60
+ if (await pathIsFile(path.join(sandboxApp, 'package.json')) && (await isMeridianeSandboxApp(sandboxApp))) {
61
+ return sandboxApp;
62
+ }
63
+
64
+ throw new Error('Missing <packageName>. Run from the sandbox app (`apps/sandbox`) or provide a package name.');
65
+ }
66
+
67
+ async function findLatestTgz(distDir) {
68
+ const entries = await fs.readdir(distDir, { withFileTypes: true }).catch(() => []);
69
+ const tgzFiles = [];
70
+ for (const e of entries) {
71
+ if (!e.isFile()) continue;
72
+ if (!e.name.endsWith('.tgz')) continue;
73
+ const full = path.join(distDir, e.name);
74
+ const st = await fs.stat(full);
75
+ tgzFiles.push({ full, mtimeMs: st.mtimeMs });
76
+ }
77
+ tgzFiles.sort((a, b) => b.mtimeMs - a.mtimeMs);
78
+ return tgzFiles[0]?.full;
79
+ }
80
+
81
+ function splitPackageName(packageName) {
82
+ const s = String(packageName || '').trim();
83
+ if (!s) throw new Error('Missing packageName');
84
+ if (s.startsWith('@')) {
85
+ const [scope, name] = s.split('/');
86
+ if (!scope || !name) throw new Error(`Invalid packageName: ${packageName}`);
87
+ return { scope, name };
88
+ }
89
+ return { scope: undefined, name: s };
90
+ }
91
+
92
+ async function resolveNodeModulesRoot(startDir) {
93
+ let cur = startDir;
94
+ for (;;) {
95
+ const candidate = path.join(cur, 'node_modules');
96
+ try {
97
+ const st = await fs.stat(candidate);
98
+ if (st.isDirectory()) return candidate;
99
+ } catch {
100
+ // ignore
101
+ }
102
+ const parent = path.dirname(cur);
103
+ if (parent === cur) break;
104
+ cur = parent;
105
+ }
106
+
107
+ const nm = path.join(startDir, 'node_modules');
108
+ await fs.mkdir(nm, { recursive: true });
109
+ return nm;
110
+ }
111
+
112
+ async function installPackageFromDist({ packageName, distDir, appRoot, log }) {
113
+ const nmRoot = await resolveNodeModulesRoot(appRoot);
114
+ const { scope, name } = splitPackageName(packageName);
115
+ const pkgDir = scope ? path.join(nmRoot, scope, name) : path.join(nmRoot, name);
116
+
117
+ log?.step?.(`installation locale dans node_modules (${scope ? `${scope}/${name}` : name})`);
118
+
119
+ await fs.rm(pkgDir, { recursive: true, force: true });
120
+ await fs.mkdir(path.dirname(pkgDir), { recursive: true });
121
+
122
+ await fs.cp(distDir, pkgDir, {
123
+ recursive: true,
124
+ filter: (src) => !src.endsWith('.tgz'),
125
+ });
126
+
127
+ return pkgDir;
128
+ }
129
+
130
+ export async function runDev(packageName, opts) {
131
+ const startCwd = process.cwd();
132
+ const debug = !!opts.debug;
133
+ const log = createLogger({ debug });
134
+
135
+ const useSandboxDefaults = packageName === undefined;
136
+ const appRoot = useSandboxDefaults ? await resolveSandboxAppRoot(startCwd) : await resolveDevAppRoot(startCwd);
137
+
138
+ const effectivePackageName = packageName ?? '@obsidiane/bridge-sandbox';
139
+ const libName = deriveLibName(effectivePackageName);
140
+
141
+ const noModels = opts.models === false;
142
+ const preset = normalizePreset(useSandboxDefaults ? (opts.preset ?? 'native') : opts.preset);
143
+ const include = splitListArgs(opts.include);
144
+ const exclude = splitListArgs(opts.exclude);
145
+
146
+ const version = '0.0.0-dev';
147
+ const requiredMode = 'all-optional';
148
+ const specSource = useSandboxDefaults ? (opts.spec ?? 'http://localhost:8000/api/docs.json') : opts.spec;
149
+
150
+ log.title('dev — build standalone + install local');
151
+ log.info(`app: ${appRoot}`);
152
+ log.info(`package: ${effectivePackageName}@${version}`);
153
+ log.info(`lib: ${libName}`);
154
+ if (useSandboxDefaults) log.info('defaults sandbox appliqués');
155
+ if (noModels) log.info('models: désactivés (--no-models)');
156
+ else log.info(`spec: ${specSource} | preset: ${preset}`);
157
+
158
+ const { workspaceRoot, distRoot, npmCacheDir, requireFromWs } = await ensureStandaloneWorkspace({ repoRoot: appRoot, log });
159
+ log.info(`workspace: ${workspaceRoot}`);
160
+
161
+ let spec;
162
+ if (!noModels) {
163
+ log.step('lecture de la spec OpenAPI');
164
+ spec = await readOpenApiSpec(specSource);
165
+ log.success('spec chargée');
166
+ }
167
+
168
+ await ensureToolchainInstalled({ workspaceRoot, requireFromWs, npmCacheDir, log });
169
+
170
+ await generateBridgeWorkspace({
171
+ cwd: workspaceRoot,
172
+ libName,
173
+ packageName: effectivePackageName,
174
+ version,
175
+ noModels,
176
+ spec,
177
+ requiredMode,
178
+ preset,
179
+ include,
180
+ exclude,
181
+ debug,
182
+ log,
183
+ distRoot,
184
+ });
185
+
186
+ const ngPackageJsonPath = path.join(workspaceRoot, 'projects', libName, 'ng-package.json');
187
+ const tsconfigDevPath = path.join(workspaceRoot, 'projects', libName, 'tsconfig.lib.json');
188
+
189
+ log.step('ng-packagr');
190
+ const ngPackagrPath = requireFromWs.resolve('ng-packagr/src/cli/main.js');
191
+ const packagrCode = await run(process.execPath, [ngPackagrPath, '-p', ngPackageJsonPath, '-c', tsconfigDevPath], {
192
+ cwd: workspaceRoot,
193
+ });
194
+ if (packagrCode !== 0) process.exit(packagrCode);
195
+
196
+ const distDir = path.join(distRoot, libName);
197
+ log.step(`npm pack (${path.relative(appRoot, distDir)})`);
198
+ const packCode = await run('npm', ['pack'], {
199
+ cwd: distDir,
200
+ env: {
201
+ ...process.env,
202
+ npm_config_cache: npmCacheDir,
203
+ npm_config_update_notifier: 'false',
204
+ npm_config_fund: 'false',
205
+ npm_config_audit: 'false',
206
+ },
207
+ });
208
+ if (packCode !== 0) process.exit(packCode);
209
+
210
+ await findLatestTgz(distDir); // keep producing the tgz in dist for inspection/use
211
+ await installPackageFromDist({ packageName: effectivePackageName, distDir, appRoot, log });
212
+
213
+ log.success('bridge installé (node_modules) ✅');
214
+ }
215
+
216
+ if (import.meta.url === `file://${process.argv[1]}`) {
217
+ throw new Error('This module is not meant to be executed directly.');
218
+ }
@@ -2,13 +2,19 @@ import fs from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
3
  import Handlebars from 'handlebars';
4
4
 
5
+ const templateCache = new Map();
6
+
5
7
  /**
6
8
  * Rend un template Handlebars avec un contexte et écrit le fichier.
7
9
  * @param {{ templatePath: string, outPath: string, ctx: any }} params
8
10
  */
9
11
  export async function renderTemplateToFile({templatePath, outPath, ctx}) {
10
- const tplSrc = await fs.readFile(templatePath, 'utf8');
11
- const tpl = Handlebars.compile(tplSrc, {noEscape: true});
12
+ let tpl = templateCache.get(templatePath);
13
+ if (!tpl) {
14
+ const tplSrc = await fs.readFile(templatePath, 'utf8');
15
+ tpl = Handlebars.compile(tplSrc, {noEscape: true});
16
+ templateCache.set(templatePath, tpl);
17
+ }
12
18
  const out = tpl(ctx);
13
19
  await fs.mkdir(path.dirname(outPath), {recursive: true});
14
20
  await fs.writeFile(outPath, out, 'utf8');
@@ -1,20 +1,26 @@
1
1
  /**
2
- * Convertit un nom de schéma OpenAPI en un nom lisible (sans artefacts jsonld/jsonapi)
3
- * et intégrant les groupes (ex: Identity.jsonld-user.read -> Identity user read)
2
+ * Construit un label "tokenisé" (sans `.` ni `-`) à partir d'un nom de schéma OpenAPI.
3
+ * Le label est ensuite passé à `sanitizeTypeName()` pour produire un identifiant TS stable.
4
+ *
4
5
  * @param {string} original
6
+ * @param {{ preset: 'all'|'native' }} cfg
5
7
  */
6
- export function friendlyName(original) {
7
- const m = original.match(/^(.*?)(?:\.(?:jsonld|jsonapi))?-(.+)$/i);
8
- if (m) {
9
- const base = m[1];
10
- const baseSimple = base.split('.').pop();
11
- const groupTokens = m[2].split('.');
12
- if (groupTokens.length && groupTokens[0].toLowerCase() === String(baseSimple).toLowerCase()) {
13
- groupTokens.shift();
14
- }
15
- const group = groupTokens.join(' ');
16
- return group ? `${base} ${group}` : base;
8
+ export function schemaLabel(original, cfg) {
9
+ const preset = cfg?.preset ?? 'all';
10
+ if (preset === 'native') {
11
+ // En native, on vise un nom "entity-like" : on ignore le groupe (après `-`) si présent.
12
+ // Les variantes .jsonld/.jsonapi/jsonMergePatch sont censées être filtrées par le preset.
13
+ return String(original).split('-')[0];
17
14
  }
18
- return original.replace(/\.(jsonld|jsonapi)$/ig, '');
19
- }
20
15
 
16
+ // En all, on encode tout : `.` et `-` deviennent des séparateurs de tokens.
17
+ const rawTokens = String(original).split(/[.-]/g).filter(Boolean);
18
+ const tokens = rawTokens.map((t) => {
19
+ const lower = t.toLowerCase();
20
+ if (lower === 'jsonld') return 'Jsonld';
21
+ if (lower === 'jsonapi') return 'Jsonapi';
22
+ if (lower === 'jsonmergepatch' || t === 'jsonMergePatch') return 'JsonMergePatch';
23
+ return t;
24
+ });
25
+ return tokens.join(' ');
26
+ }
@@ -1,46 +1,52 @@
1
1
  import { sanitizeTypeName, quoteKeyIfNeeded } from './utils.js';
2
- import { filterSchemaNames, mergeAllOf, isObject } from './schema-utils.js';
3
- import { friendlyName } from './naming.js';
2
+ import { filterSchemaNames, applySchemaNameFilters, mergeAllOf, isObject, schemaNameFromRef } from './schema-utils.js';
3
+ import { schemaLabel } from './naming.js';
4
4
  import { tsTypeOf, withNullable } from './type-resolver.js';
5
5
 
6
+ /**
7
+ * @typedef {{
8
+ * requiredMode?: 'all-optional'|'spec',
9
+ * preset?: 'all'|'native',
10
+ * includeSchemaNames?: Array<RegExp|string|((name: string) => boolean)>,
11
+ * excludeSchemaNames?: Array<RegExp|string|((name: string) => boolean)>,
12
+ * }} BuildOptions
13
+ */
14
+
6
15
  /**
7
16
  * Construit une map originalName -> sanitizedName, en assurant l’unicité
8
17
  * @param {string[]} schemaNames
9
18
  */
10
- function buildNameMap(schemaNames) {
11
- const parse = (original) => {
12
- const m = original.match(/^(.*?)(?:\.(?:jsonld|jsonapi))?-(.+)$/i);
13
- return m ? { base: m[1], group: m[2] } : { base: original, group: null };
14
- };
15
-
16
- const parsed = schemaNames.map((n) => ({ name: n, ...parse(n) }));
17
- const roots = parsed.filter((p) => !p.group);
18
- const grouped = parsed.filter((p) => !!p.group);
19
-
20
- // Root sanitized names (to avoid collisions when dropping group)
21
- const rootSanitized = new Set(roots.map((p) => sanitizeTypeName(p.base, new Set())));
22
-
23
- // Count base candidates among grouped to see if unique
24
- const baseCount = new Map();
25
- for (const p of grouped) {
26
- const cand = sanitizeTypeName(p.base, new Set());
27
- baseCount.set(cand, (baseCount.get(cand) || 0) + 1);
28
- }
19
+ function normalizeNativeBase(original) {
20
+ return String(original)
21
+ .replace(/\.jsonld\b/ig, '')
22
+ .replace(/\.jsonapi\b/ig, '')
23
+ .replace(/\.jsonmergepatch\b/ig, '')
24
+ .split('-')[0];
25
+ }
29
26
 
30
- // Build final map with uniqueness
27
+ function buildNameMap(schemaNames, schemas, preset) {
28
+ // Build final map with uniqueness (deterministic order)
31
29
  const used = new Set();
32
30
  const nameMap = new Map();
33
- for (const p of parsed) {
34
- let label;
35
- if (p.group) {
36
- const cand = sanitizeTypeName(p.base, new Set());
37
- const isUniqueBase = (baseCount.get(cand) === 1) && !rootSanitized.has(cand);
38
- label = isUniqueBase ? p.base : friendlyName(p.name);
39
- } else {
40
- label = friendlyName(p.name);
31
+ for (const original of schemaNames) {
32
+ const label = schemaLabel(original, {preset});
33
+ nameMap.set(original, sanitizeTypeName(label, used));
34
+ }
35
+
36
+ // In native mode, alias excluded variants to the selected base type
37
+ // so `$ref` doesn't leak "User-user.write" etc into generated types.
38
+ if (preset === 'native') {
39
+ const baseToSanitized = new Map();
40
+ for (const original of schemaNames) {
41
+ baseToSanitized.set(normalizeNativeBase(original), nameMap.get(original));
42
+ }
43
+ for (const original of Object.keys(schemas)) {
44
+ const base = normalizeNativeBase(original);
45
+ const sanitized = baseToSanitized.get(base);
46
+ if (sanitized) nameMap.set(original, sanitized);
41
47
  }
42
- nameMap.set(p.name, sanitizeTypeName(label, used));
43
48
  }
49
+
44
50
  return nameMap;
45
51
  }
46
52
 
@@ -53,8 +59,9 @@ function buildNameMap(schemaNames) {
53
59
  export function buildModelsFromOpenAPI(spec, options) {
54
60
  const opts = { requiredMode: 'all-optional', ...(options || {}) };
55
61
  const schemas = spec?.components?.schemas || {};
56
- const schemaNames = filterSchemaNames(schemas, opts);
57
- const nameMap = buildNameMap(schemaNames);
62
+ const schemaNames = applySchemaNameFilters(filterSchemaNames(schemas, opts), opts);
63
+ const preset = opts.preset ?? 'all';
64
+ const nameMap = buildNameMap(schemaNames, schemas, preset);
58
65
  const allSanitizedNames = new Set([...nameMap.values()]);
59
66
 
60
67
  /** @type {ModelDefinition[]} */
@@ -62,19 +69,49 @@ export function buildModelsFromOpenAPI(spec, options) {
62
69
 
63
70
  for (const originalName of schemaNames) {
64
71
  const raw = schemas[originalName];
72
+ const sanitized = nameMap.get(originalName);
73
+ if (!sanitized) continue;
74
+
75
+ /** @type {string[]} */
76
+ const extendsTypes = [];
77
+
65
78
  let effective = raw;
66
79
  if (raw?.allOf) {
67
- const merged = mergeAllOf(raw.allOf, opts);
68
- if (merged) effective = merged;
80
+ const merged = mergeAllOf(raw.allOf);
81
+ if (merged) {
82
+ effective = merged;
83
+ } else {
84
+ // Non-flattenable allOf (refs, mixed forms): keep inline object props and model refs as extends.
85
+ const props = {};
86
+ const required = new Set();
87
+ const ext = new Set();
88
+
89
+ for (const part of raw.allOf) {
90
+ if (part?.$ref) {
91
+ const orig = schemaNameFromRef(part.$ref);
92
+ const refName = orig ? nameMap.get(orig) : undefined;
93
+ if (refName && refName !== sanitized && allSanitizedNames.has(refName)) ext.add(refName);
94
+ continue;
95
+ }
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
+
102
+ extendsTypes.push(...[...ext].sort((a, b) => a.localeCompare(b)));
103
+ effective = { type: 'object', properties: props, required: [...required] };
104
+ }
69
105
  }
70
106
 
71
- const isObjectLike = effective?.type === 'object' || isObject(effective?.properties);
107
+ const isObjectLike = effective?.type === 'object' || isObject(effective?.properties) || extendsTypes.length > 0;
72
108
  if (!isObjectLike) continue;
73
109
 
74
- const sanitized = nameMap.get(originalName);
75
110
  const required = new Set(effective.required || []);
76
111
  const deps = new Set();
77
112
 
113
+ for (const e of extendsTypes) deps.add(e);
114
+
78
115
  const props = Object.entries(effective.properties || {}).map(([propName, prop]) => {
79
116
  const baseType = tsTypeOf(prop, deps, nameMap);
80
117
  const type = withNullable(baseType, prop);
@@ -91,7 +128,7 @@ export function buildModelsFromOpenAPI(spec, options) {
91
128
  .filter((n) => allSanitizedNames.has(n))
92
129
  .sort((a, b) => a.localeCompare(b));
93
130
 
94
- models.push({name: sanitized, props, imports});
131
+ models.push({name: sanitized, props, imports, extendsTypes});
95
132
  }
96
133
 
97
134
  return {models};
@@ -1,16 +1,5 @@
1
- /** Hydra base schemas to ignore in merges and filtering */
2
- const DEFAULT_HYDRA_BASE_RE = /^Hydra(?:Item|Collection)BaseSchema$/;
3
-
4
- /**
5
- * Récupère la RegExp Hydra à partir d'une config optionnelle.
6
- * @param {{ hydraBaseRegex?: RegExp|string }} [cfg]
7
- */
8
- function hydraRe(cfg) {
9
- const r = cfg?.hydraBaseRegex;
10
- if (!r) return DEFAULT_HYDRA_BASE_RE;
11
- if (r instanceof RegExp) return r;
12
- try { return new RegExp(r); } catch { return DEFAULT_HYDRA_BASE_RE; }
13
- }
1
+ /** Hydra base schemas to ignore in safe `allOf` merges */
2
+ const HYDRA_BASE_RE = /^Hydra(?:Item|Collection)BaseSchema$/;
14
3
 
15
4
  /**
16
5
  * Détermine si une valeur est un objet simple (non tableau)
@@ -37,12 +26,12 @@ export function schemaNameFromRef($ref) {
37
26
  * @param {any[]} schemas
38
27
  * @returns {any|null}
39
28
  */
40
- export function mergeAllOf(schemas, cfg) {
29
+ export function mergeAllOf(schemas) {
41
30
  const out = {type: 'object', properties: {}, required: []};
42
31
  for (const s of schemas) {
43
32
  if (s.$ref) {
44
33
  const ref = schemaNameFromRef(s.$ref);
45
- if (hydraRe(cfg).test(ref)) {
34
+ if (HYDRA_BASE_RE.test(ref)) {
46
35
  continue;
47
36
  }
48
37
  return null;
@@ -59,67 +48,96 @@ export function mergeAllOf(schemas, cfg) {
59
48
  }
60
49
 
61
50
  /**
62
- * Filtre et déduplique les noms de schémas à générer.
63
- * Préférence entre variantes: jsonld > none > jsonapi.
64
- * - Exclut HydraItem/Collection base schemas
65
- * - Garde les noms groupés (avec `-`) en choisissant la meilleure variante
66
- * - Garde les racines sans point et sans suffixe jsonld/jsonapi
51
+ * Liste les noms de schémas dans `components.schemas`.
52
+ * La sélection/filtres est gérée par `applySchemaNameFilters`.
67
53
  * @param {Record<string, any>} schemas
68
54
  * @returns {string[]}
69
55
  */
70
56
  export function filterSchemaNames(schemas, cfg) {
71
- const prefer = (cfg?.preferFlavor === 'jsonapi') ? ['jsonapi','none','jsonld']
72
- : (cfg?.preferFlavor === 'none') ? ['none','jsonld','jsonapi']
73
- : ['jsonld','none','jsonapi']; // default jsonld
74
- const flavorOf = (name) => /\.jsonld\b/i.test(name) ? 'jsonld' : /\.jsonapi\b/i.test(name) ? 'jsonapi' : 'none';
75
- const rank = (name) => 3 - prefer.indexOf(flavorOf(name));
57
+ return Object.keys(schemas).sort((a, b) => a.localeCompare(b));
58
+ }
59
+
60
+ /**
61
+ * @param {unknown} rule
62
+ * @returns {rule is ((name: string) => boolean)}
63
+ */
64
+ function isRuleFn(rule) {
65
+ return typeof rule === 'function';
66
+ }
67
+
68
+ /**
69
+ * @param {string} name
70
+ * @param {RegExp|string|((name: string) => boolean)} rule
71
+ */
72
+ function matchesRule(name, rule) {
73
+ if (rule instanceof RegExp) return rule.test(name);
74
+ if (isRuleFn(rule)) return !!rule(name);
75
+ if (typeof rule === 'string' && rule.length) return name.includes(rule);
76
+ return false;
77
+ }
76
78
 
77
- const roots = [];
78
- const grouped = new Map();
79
- const variantsNoGroup = new Map(); // base -> best variant (no '-')
79
+ /**
80
+ * Applique un preset de génération et/ou des règles include/exclude.
81
+ *
82
+ * - `preset: "all"`: ne filtre rien (comportement par défaut)
83
+ * - `preset: "native"`: retire les schémas "techniques" (Hydra*, jsonMergePatch…)
84
+ *
85
+ * Les règles `includeSchemaNames` / `excludeSchemaNames` s'appliquent sur les noms
86
+ * de schémas OpenAPI (ex: "User-user.read", "Conversation.jsonMergePatch", …).
87
+ *
88
+ * @param {string[]} schemaNames
89
+ * @param {{
90
+ * preset?: 'all'|'native',
91
+ * includeSchemaNames?: Array<RegExp|string|((name: string) => boolean)>,
92
+ * excludeSchemaNames?: Array<RegExp|string|((name: string) => boolean)>,
93
+ * }} [cfg]
94
+ */
95
+ export function applySchemaNameFilters(schemaNames, cfg) {
96
+ const preset = cfg?.preset ?? 'all';
80
97
 
81
- for (const n of Object.keys(schemas)) {
82
- if (hydraRe(cfg).test(n)) continue;
98
+ /** @type {Array<RegExp|string|((name: string) => boolean)>} */
99
+ const include = Array.isArray(cfg?.includeSchemaNames) ? cfg.includeSchemaNames : [];
100
+ /** @type {Array<RegExp|string|((name: string) => boolean)>} */
101
+ const exclude = Array.isArray(cfg?.excludeSchemaNames) ? [...cfg.excludeSchemaNames] : [];
83
102
 
84
- const hasGroup = n.includes('-');
85
- const hasDot = n.includes('.');
86
- const isJsonFlavor = /\.jsonld\b|\.jsonapi\b/i.test(n);
103
+ // 1) include/exclude user filters (always applied first)
104
+ const hasInclude = include.length > 0;
105
+ let out = schemaNames.filter((name) => {
106
+ if (hasInclude && !include.some((r) => matchesRule(name, r))) return false;
107
+ if (exclude.some((r) => matchesRule(name, r))) return false;
108
+ return true;
109
+ });
87
110
 
88
- if (!hasGroup) {
89
- // Cas 1: racine simple (pas de point, pas de variante jsonld/jsonapi)
90
- if (!hasDot && !isJsonFlavor) {
91
- roots.push(n);
92
- continue;
93
- }
94
- // Cas 2: variante jsonld/jsonapi sans groupe (ex: Workflow.WorkflowInput.jsonld ou Foo.jsonld)
95
- if (isJsonFlavor) {
96
- const base = n.replace(/\.(jsonld|jsonapi)\b/i, '');
97
- // Si une base "pure" existe dans la spec, on préfère l’ignorer pour éviter les doublons
98
- if (Object.prototype.hasOwnProperty.call(schemas, base)) {
99
- continue;
100
- }
101
- const curr = variantsNoGroup.get(base);
102
- if (!curr || rank(n) > rank(curr)) variantsNoGroup.set(base, n);
111
+ // 2) preset filters
112
+ if (preset === 'native') {
113
+ out = out
114
+ .filter((n) => !/^Hydra/i.test(n))
115
+ .filter((n) => !/jsonMergePatch/i.test(n))
116
+ .filter((n) => !/\.jsonld\b/i.test(n))
117
+ .filter((n) => !/\.jsonapi\b/i.test(n));
118
+
119
+ // 3) collapse group variants: keep one schema per base (before `-`)
120
+ const score = (name) => {
121
+ if (!name.includes('-')) return 100;
122
+ const group = name.split('-').slice(1).join('-');
123
+ if (/(^|[.\-])read($|[.\-])/i.test(group)) return 50;
124
+ return 0;
125
+ };
126
+ const byBase = new Map();
127
+ for (const name of out) {
128
+ const base = name.split('-')[0];
129
+ const prev = byBase.get(base);
130
+ if (!prev) {
131
+ byBase.set(base, name);
103
132
  continue;
104
133
  }
105
- // Cas 3: nom avec point mais sans suffixe jsonld/jsonapi et sans group — on le considère tel quel
106
- variantsNoGroup.set(n, n);
107
- continue;
108
- }
109
-
110
- const m = n.match(/^(.*?)(?:\.(?:jsonld|jsonapi))?-(.+)$/i);
111
- if (!m) {
112
- const curr = grouped.get(n);
113
- if (!curr || rank(n) > rank(curr)) grouped.set(n, n);
114
- continue;
134
+ const a = prev, b = name;
135
+ const sa = score(a), sb = score(b);
136
+ if (sb > sa) byBase.set(base, b);
137
+ else if (sb === sa && b.localeCompare(a) < 0) byBase.set(base, b);
115
138
  }
116
- const base = m[1];
117
- const group = m[2];
118
- const key = `${base}|${group}`;
119
- const curr = grouped.get(key);
120
- if (!curr || rank(n) > rank(curr)) grouped.set(key, n);
139
+ out = [...byBase.values()].sort((a, b) => a.localeCompare(b));
121
140
  }
122
- // Ajoute les variantes sans groupe sélectionnées
123
- const noGroupSelected = [...variantsNoGroup.values()];
124
- return [...roots, ...[...grouped.values()], ...noGroupSelected].sort((a, b) => a.localeCompare(b));
141
+
142
+ return out;
125
143
  }
@@ -1,9 +1,9 @@
1
- import { Item } from '{{itemImportPath}}';
1
+ import type { Item } from '../lib/ports/resource-repository.port';
2
2
  {{#each imports}}
3
- import { {{this}} } from './{{this}}';
3
+ import type { {{this}} } from './{{this}}';
4
4
  {{/each}}
5
5
 
6
- export interface {{name}} extends Item {
6
+ export interface {{name}} extends Item{{#each extendsTypes}}, {{this}}{{/each}} {
7
7
  {{#each props}}
8
8
  {{{tsKey}}}{{#if optional}}?{{/if}}: {{type}};
9
9
  {{/each}}
@@ -33,8 +33,7 @@ export function pascalCase(tokens) {
33
33
  * @param {Set<string>} used
34
34
  */
35
35
  export function sanitizeTypeName(original, used) {
36
- const base = original.includes('.') ? original.split('.').pop() : original;
37
- const tokens = base.split(/[^A-Za-z0-9]+/);
36
+ const tokens = String(original).split(/[^A-Za-z0-9]+/);
38
37
  let name = pascalCase(tokens) || 'Model';
39
38
  if (/^[0-9]/.test(name)) name = '_' + name;
40
39
  if (RESERVED.has(name)) name = name + 'Model';