@obsidiane/meridiane 0.0.6 → 0.0.7

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 (37) hide show
  1. package/README.md +42 -2
  2. package/cli.js +27 -1
  3. package/models.config.example.js +15 -0
  4. package/package.json +1 -3
  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/public-api.ts +3 -2
  26. package/tools/dev-bridge.js +138 -0
  27. package/tools/generate-lib.js +60 -45
  28. package/tools/generate-models.js +73 -19
  29. package/tools/generator/models/openapi-to-models.js +13 -2
  30. package/tools/generator/models/schema-utils.js +59 -0
  31. package/tools/generator/models/utils.js +8 -2
  32. package/tools/init.js +148 -0
  33. package/tools/sandbox-bridge.js +77 -0
  34. package/tools/utils/cli-args.js +41 -0
  35. package/tools/utils/dotenv.js +55 -0
  36. package/templates/_lib_template/src/lib/bridge/credentials.policy.ts +0 -9
  37. package/templates/_lib_template/src/lib/utils/utils.ts +0 -21
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env node
2
+ import process from 'node:process';
3
+ import path from 'node:path';
4
+ import {fileURLToPath} from 'node:url';
5
+ import {spawn} from 'node:child_process';
6
+ import {loadDotEnv} from './utils/dotenv.js';
7
+ import {getArg, getArgs, hasFlag} from './utils/cli-args.js';
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+
11
+ function normalizeBaseUrl(u) {
12
+ if (!u) return u;
13
+ return u.endsWith('/') ? u.slice(0, -1) : u;
14
+ }
15
+
16
+ function joinUrl(base, p) {
17
+ const b = normalizeBaseUrl(base);
18
+ if (!b) return p;
19
+ if (/^https?:\/\//i.test(p)) return p;
20
+ if (!p.startsWith('/')) return `${b}/${p}`;
21
+ return `${b}${p}`;
22
+ }
23
+
24
+ function runNode(scriptPath, args, {cwd} = {}) {
25
+ return new Promise((resolve) => {
26
+ const child = spawn(process.execPath, [scriptPath, ...args], {
27
+ stdio: 'inherit',
28
+ env: process.env,
29
+ cwd: cwd ?? process.cwd(),
30
+ });
31
+ child.on('exit', (code) => resolve(code ?? 0));
32
+ });
33
+ }
34
+
35
+ function printHelp() {
36
+ console.log(`
37
+ Usage:
38
+ meridiane dev-bridge <lib-name> <npm-package-name> [version]
39
+
40
+ Options:
41
+ --workspace=apps/sandbox Working directory (default: current directory)
42
+ --backend=http://localhost:8000 Base URL of the backend (default: http://localhost:8000)
43
+ --spec=/api/docs.json Spec path or full URL (default: /api/docs.json)
44
+ --models-out=projects/<lib>/src/models Output dir for generated models
45
+ --required-mode=all-optional|spec Models required-mode (default: all-optional)
46
+ --preset=all|native Models preset (default: all)
47
+ --include=<substr>[,<substr>…] Include schema names (repeatable)
48
+ --exclude=<substr>[,<substr>…] Exclude schema names (repeatable)
49
+ --index=1|0 Generate index.ts (default: 1)
50
+ --no-models Skip models generation
51
+ --debug Enable debug logs
52
+
53
+ Examples:
54
+ npx -y @obsidiane/meridiane dev-bridge bridge-sandbox @obsidiane/bridge-sandbox 0.1.0
55
+ npx -y @obsidiane/meridiane dev-bridge bridge-sandbox @obsidiane/bridge-sandbox 0.1.0 --workspace=apps/sandbox
56
+ npx -y @obsidiane/meridiane dev-bridge backend-bridge @acme/backend-bridge 0.1.0 --backend=http://localhost:8000 --workspace=apps/sandbox
57
+ `);
58
+ }
59
+
60
+ async function main() {
61
+ await loadDotEnv();
62
+
63
+ const debug = hasFlag('debug') || /^(1|true|yes)$/i.test(process.env.MERIDIANE_DEBUG ?? '');
64
+ if (debug) process.env.MERIDIANE_DEBUG = '1';
65
+
66
+ const positional = process.argv.slice(2).filter((a) => !a.startsWith('--'));
67
+ const [libName, packageName, versionArg] = positional;
68
+ if (!libName || !packageName) {
69
+ printHelp();
70
+ process.exit(1);
71
+ }
72
+
73
+ const workspaceArg = getArg('workspace', process.env.MERIDIANE_DEV_WORKSPACE ?? '.');
74
+ const workspaceDir = path.resolve(process.cwd(), workspaceArg);
75
+
76
+ const backend = normalizeBaseUrl(getArg('backend', process.env.MERIDIANE_DEV_BACKEND ?? 'http://localhost:8000'));
77
+ const specArg = getArg('spec', process.env.MERIDIANE_DEV_SPEC ?? '/api/docs.json');
78
+ const specUrl = /^https?:\/\//i.test(specArg) ? specArg : joinUrl(backend, specArg);
79
+
80
+ const version = versionArg ?? process.env.MERIDIANE_LIB_VERSION ?? '0.0.1';
81
+ const urlRegistry = getArg('registry', process.env.MERIDIANE_NPM_REGISTRY_URL);
82
+
83
+ const toolsDir = __dirname;
84
+ const genLib = path.join(toolsDir, 'generate-lib.js');
85
+ const genModels = path.join(toolsDir, 'generate-models.js');
86
+
87
+ if (debug) {
88
+ console.log('[meridiane dev-bridge] config', {
89
+ workspaceDir,
90
+ backend,
91
+ specUrl,
92
+ libName,
93
+ packageName,
94
+ version,
95
+ urlRegistry: urlRegistry ? '<set>' : '<unset>',
96
+ });
97
+ }
98
+
99
+ // 1) Regenerate the library skeleton.
100
+ const libArgs = [libName, packageName, version, ...(urlRegistry ? [urlRegistry] : [])];
101
+ const libCode = await runNode(genLib, libArgs, {cwd: workspaceDir});
102
+ if (libCode !== 0) process.exit(libCode);
103
+
104
+ // 2) Generate models from the backend spec (dev-only convenience).
105
+ if (hasFlag('no-models')) return;
106
+
107
+ const modelsOut = getArg('models-out', `projects/${libName}/src/models`);
108
+ const requiredMode = getArg('required-mode', process.env.MERIDIANE_MODELS_REQUIRED_MODE ?? 'all-optional');
109
+ const preset = getArg('preset', process.env.MERIDIANE_MODELS_PRESET);
110
+ const include = getArgs('include');
111
+ const exclude = getArgs('exclude');
112
+ const index = getArg('index', process.env.MERIDIANE_MODELS_INDEX);
113
+
114
+ const modelsArgs = [
115
+ specUrl,
116
+ `--out=${modelsOut}`,
117
+ `--required-mode=${requiredMode}`,
118
+ ...(preset ? [`--preset=${preset}`] : []),
119
+ ...include.map((v) => `--include=${v}`),
120
+ ...exclude.map((v) => `--exclude=${v}`),
121
+ ...(index ? [`--index=${index}`] : []),
122
+ ];
123
+
124
+ // Try the configured URL first, then fallback to API Platform alternative endpoint.
125
+ let modelsCode = await runNode(genModels, modelsArgs, {cwd: workspaceDir});
126
+ if (modelsCode !== 0 && specUrl.endsWith('/api/docs.json')) {
127
+ const fallback = specUrl.replace(/\/api\/docs\.json$/, '/api/docs.jsonopenapi');
128
+ console.warn(`⚠️ Models generation failed for ${specUrl}, retrying with ${fallback}`);
129
+ modelsCode = await runNode(genModels, [fallback, ...modelsArgs.slice(1)], {cwd: workspaceDir});
130
+ }
131
+
132
+ process.exit(modelsCode);
133
+ }
134
+
135
+ main().catch((err) => {
136
+ console.error(err);
137
+ process.exit(1);
138
+ });
@@ -1,17 +1,16 @@
1
1
  #!/usr/bin/env node
2
- import fs from "fs-extra"
3
- import path from "path"
4
- import { fileURLToPath } from 'url';
2
+ import fs from 'node:fs/promises';
3
+ import {existsSync} from 'node:fs';
4
+ import path from 'node:path';
5
+ import {fileURLToPath} from 'node:url';
6
+
7
+ import {loadDotEnv} from './utils/dotenv.js';
5
8
 
6
9
  const __filename = fileURLToPath(import.meta.url);
7
- const __dirname = path.dirname(__filename);
8
- /**
9
- * Recursively replace all placeholders in files under a directory
10
- * @param {string} srcDir - source directory path
11
- * @param {Object.<string,string>} placeholders - map of placeholder -> value
12
- */
10
+ const __dirname = path.dirname(__filename);
11
+
13
12
  async function replacePlaceholdersInDir(srcDir, placeholders) {
14
- const entries = await fs.readdir(srcDir, { withFileTypes: true });
13
+ const entries = await fs.readdir(srcDir, {withFileTypes: true});
15
14
  for (const entry of entries) {
16
15
  const fullPath = path.join(srcDir, entry.name);
17
16
  if (entry.isDirectory()) {
@@ -27,33 +26,37 @@ async function replacePlaceholdersInDir(srcDir, placeholders) {
27
26
  }
28
27
  }
29
28
 
30
- /**
31
- * Generate a new Angular library from template
32
- * @param {string} libName - folder and project name
33
- * @param {string} packageName - NPM package name (e.g. @org/lib)
34
- * @param {string} version - initial version for the package
35
- * @param {string} urlRegistry - NPM registry url
36
- */
29
+ async function readJson(filePath) {
30
+ const raw = await fs.readFile(filePath, 'utf8');
31
+ return JSON.parse(raw);
32
+ }
33
+
34
+ async function writeJson(filePath, data) {
35
+ await fs.writeFile(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
36
+ }
37
+
37
38
  async function generate(libName, packageName, version, urlRegistry) {
38
39
  const tplDir = path.resolve(__dirname, '../templates/_lib_template');
39
40
  const workspaceRoot = process.cwd();
40
41
  const targetDir = path.resolve(workspaceRoot, 'projects', libName);
41
42
 
42
43
  // 1) Copy the template directory
43
- await fs.copy(tplDir, targetDir);
44
+ await fs.rm(targetDir, {recursive: true, force: true});
45
+ await fs.mkdir(targetDir, {recursive: true});
46
+ await fs.cp(tplDir, targetDir, {recursive: true});
44
47
 
45
48
  // 2) Replace placeholders __LIB_NAME__, __PACKAGE_NAME__, __VERSION__
46
49
  const placeholders = {
47
50
  '__LIB_NAME__': libName,
48
51
  '__PACKAGE_NAME__': packageName,
49
- '__VERSION__': version
52
+ '__VERSION__': version,
50
53
  };
51
54
  await replacePlaceholdersInDir(targetDir, placeholders);
52
55
 
53
56
  // 3) Update the library's package.json
54
57
  const libPackageJsonPath = path.join(targetDir, 'package.json');
55
- if (await fs.pathExists(libPackageJsonPath)) {
56
- const libPkg = await fs.readJson(libPackageJsonPath);
58
+ try {
59
+ const libPkg = await readJson(libPackageJsonPath);
57
60
  libPkg.name = packageName;
58
61
  libPkg.version = version;
59
62
  if (urlRegistry) {
@@ -62,39 +65,41 @@ async function generate(libName, packageName, version, urlRegistry) {
62
65
  delete libPkg.publishConfig.registry;
63
66
  if (Object.keys(libPkg.publishConfig).length === 0) delete libPkg.publishConfig;
64
67
  }
65
- await fs.writeJson(libPackageJsonPath, libPkg, { spaces: 2 });
68
+ await writeJson(libPackageJsonPath, libPkg);
69
+ } catch {
70
+ // ignore: missing or invalid package.json in template
66
71
  }
67
72
 
68
73
  // 4) Update angular.json
69
74
  const angularJsonPath = path.resolve(workspaceRoot, 'angular.json');
70
- const ng = await fs.readJson(angularJsonPath);
75
+ const ng = await readJson(angularJsonPath);
71
76
 
72
77
  ng.projects[libName] = {
73
78
  root: `projects/${libName}`,
74
79
  sourceRoot: `projects/${libName}/src`,
75
80
  projectType: 'library',
76
- "architect": {
77
- "build": {
78
- "builder": "@angular/build:ng-packagr",
79
- "configurations": {
80
- "production": {
81
- "tsConfig": `projects/${libName}/tsconfig.lib.prod.json`
81
+ architect: {
82
+ build: {
83
+ builder: '@angular/build:ng-packagr',
84
+ configurations: {
85
+ production: {
86
+ tsConfig: `projects/${libName}/tsconfig.lib.prod.json`,
87
+ },
88
+ development: {
89
+ tsConfig: `projects/${libName}/tsconfig.lib.json`,
82
90
  },
83
- "development": {
84
- "tsConfig": `projects/${libName}/tsconfig.lib.json`
85
- }
86
91
  },
87
- "defaultConfiguration": "production"
92
+ defaultConfiguration: 'production',
88
93
  },
89
- "test": {
90
- "builder": "@angular/build:karma",
91
- "options": {
92
- "tsConfig": `projects/${libName}/tsconfig.spec.json`
93
- }
94
- }
95
- }
94
+ test: {
95
+ builder: '@angular/build:karma',
96
+ options: {
97
+ tsConfig: `projects/${libName}/tsconfig.spec.json`,
98
+ },
99
+ },
100
+ },
96
101
  };
97
- await fs.writeJson(angularJsonPath, ng, { spaces: 2 });
102
+ await writeJson(angularJsonPath, ng);
98
103
 
99
104
  console.log(`✅ Library '${libName}' generated at projects/${libName}` +
100
105
  ` with package '${packageName}@${version}'`);
@@ -115,22 +120,32 @@ function findUpNodeModulePackageJson(startDir, packageName) {
115
120
  let dir = startDir;
116
121
  while (true) {
117
122
  const candidate = path.join(dir, 'node_modules', packageName, 'package.json');
118
- if (fs.existsSync(candidate)) return candidate;
123
+ if (existsSync(candidate)) return candidate;
119
124
  const parent = path.dirname(dir);
120
125
  if (parent === dir) return undefined;
121
126
  dir = parent;
122
127
  }
123
128
  }
124
129
 
125
- // CLI args: node generate-lib.js <lib-name> <package-name> [version] <url-registry>
130
+ await loadDotEnv();
131
+ const debug = process.argv.includes('--debug') || /^(1|true|yes)$/i.test(process.env.MERIDIANE_DEBUG ?? '');
132
+
126
133
  // CLI args: node generate-lib.js <lib-name> <package-name> [version] [url-registry]
127
- const [,, libName, packageName, version = '0.0.1', urlRegistry] = process.argv;
134
+ // + support .env via MERIDIANE_LIB_VERSION / MERIDIANE_NPM_REGISTRY_URL
135
+ const positional = process.argv.slice(2).filter((a) => !a.startsWith('--'));
136
+ const [libName, packageName, versionArg, urlRegistryArg] = positional;
137
+ const version = versionArg ?? process.env.MERIDIANE_LIB_VERSION ?? '0.0.1';
138
+ const urlRegistry = urlRegistryArg ?? process.env.MERIDIANE_NPM_REGISTRY_URL;
128
139
  if (!libName || !packageName) {
129
140
  console.error('Usage: generate-lib.js <lib-name> <package-name> [version] [url-registry]');
130
141
  process.exit(1);
131
142
  }
132
143
 
133
- generate(libName, packageName, version, urlRegistry).catch(err => {
144
+ if (debug) {
145
+ console.log('[meridiane lib] debug', {libName, packageName, version, urlRegistry: urlRegistry ? '<set>' : '<unset>'});
146
+ }
147
+
148
+ generate(libName, packageName, version, urlRegistry).catch((err) => {
134
149
  console.error(err);
135
150
  process.exit(1);
136
151
  });
@@ -7,20 +7,11 @@ import {fileURLToPath, pathToFileURL} from 'node:url';
7
7
  import {renderTemplateToFile} from './generator/models/handlebars.js';
8
8
  import {buildModelsFromOpenAPI} from './generator/models/openapi-to-models.js';
9
9
  import {ensureCleanDir} from './generator/models/utils.js';
10
+ import {asBool, getArg, getArgs} from './utils/cli-args.js';
11
+ import {loadDotEnv} from './utils/dotenv.js';
10
12
 
11
13
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
14
 
13
- /**
14
- * Récupère un argument CLI de la forme --name=value
15
- * @param {string} name
16
- * @param {string|undefined} def
17
- * @returns {string|undefined}
18
- */
19
- function getArg(name, def) {
20
- const a = process.argv.find((x) => x.startsWith(`--${name}=`));
21
- return a ? a.split('=')[1] : def;
22
- }
23
-
24
15
  /**
25
16
  * Lit la specification OpenAPI depuis une URL http(s) ou un chemin fichier JSON.
26
17
  * @param {string} specPathOrUrl
@@ -52,41 +43,104 @@ async function readSpec(specPathOrUrl) {
52
43
  * Le module peut exporter par défaut ou via export nommé.
53
44
  */
54
45
  async function loadUserConfig() {
55
- const cfgPath = path.resolve(process.cwd(), 'models.config.js');
46
+ const cfgPath = await findUp(process.cwd(), 'models.config.js');
56
47
  try {
48
+ if (!cfgPath) return {};
57
49
  const st = await fs.stat(cfgPath);
58
50
  if (!st.isFile()) return {};
59
51
  const mod = await import(pathToFileURL(cfgPath).href);
60
- return (mod?.default && typeof mod.default === 'object') ? mod.default : (mod || {});
52
+ const cfg = mod?.default ?? mod;
53
+ return (cfg && typeof cfg === 'object') ? cfg : {};
61
54
  } catch {
62
55
  return {};
63
56
  }
64
57
  }
58
+
59
+ async function findUp(startDir, fileName) {
60
+ let dir = startDir;
61
+ while (true) {
62
+ const candidate = path.join(dir, fileName);
63
+ try {
64
+ const st = await fs.stat(candidate);
65
+ if (st.isFile()) return candidate;
66
+ } catch {
67
+ // ignore
68
+ }
69
+ const parent = path.dirname(dir);
70
+ if (parent === dir) return undefined;
71
+ dir = parent;
72
+ }
73
+ }
65
74
  async function main() {
75
+ await loadDotEnv();
76
+ const debug = process.argv.includes('--debug') || /^(1|true|yes)$/i.test(process.env.MERIDIANE_DEBUG ?? '');
77
+
66
78
  const specPathOrUrl = process.argv[2];
67
79
  if (!specPathOrUrl) {
68
- console.error('Usage: generate-models.js <OpenAPI spec (url|fichier)> [--item-import=../lib/ports/resource-repository.port] [--out=models] [--required-mode=all-optional|spec] [--no-index]');
80
+ console.error('Usage: generate-models.js <OpenAPI spec (url|fichier)> [--item-import=../lib/ports/resource-repository.port] [--out=models] [--required-mode=all-optional|spec] [--preset=all|native] [--include=<substr>] [--exclude=<substr>] [--index=1|0]');
69
81
  process.exit(1);
70
82
  }
71
83
 
72
84
  const workDir = process.cwd();
73
85
  const userCfg = await loadUserConfig();
74
- const outDir = path.resolve(workDir, getArg('out', userCfg.outDir || 'models'));
75
- const writeIndex = !('' + process.argv.join(' ')).includes('--no-index');
76
- const itemImportPath = getArg('item-import', userCfg.itemImportPath || '../lib/ports/resource-repository.port');
86
+ const outDir = path.resolve(workDir, getArg('out', userCfg.outDir || process.env.MERIDIANE_MODELS_OUT || 'models'));
87
+ const writeIndex = (() => {
88
+ const cli = getArg('index', undefined);
89
+ if (typeof cli === 'string') return asBool(cli, true);
90
+ if (typeof userCfg.writeIndex === 'boolean') return userCfg.writeIndex;
91
+ return asBool(process.env.MERIDIANE_MODELS_INDEX ?? '1', true);
92
+ })();
93
+ const itemImportPath = getArg(
94
+ 'item-import',
95
+ userCfg.itemImportPath || process.env.MERIDIANE_MODELS_ITEM_IMPORT || '../lib/ports/resource-repository.port'
96
+ );
77
97
  const requiredMode = (() => {
78
- const v = getArg('required-mode', userCfg.requiredMode || 'all-optional');
98
+ const v = getArg('required-mode', userCfg.requiredMode || process.env.MERIDIANE_MODELS_REQUIRED_MODE || 'all-optional');
79
99
  return (v === 'spec' || v === 'all-optional') ? v : 'all-optional';
80
100
  })();
101
+ const preset = (() => {
102
+ const v = getArg('preset', userCfg.preset || process.env.MERIDIANE_MODELS_PRESET || 'all');
103
+ return (v === 'native' || v === 'all') ? v : 'all';
104
+ })();
81
105
  const preferFlavor = userCfg.preferFlavor; // 'jsonld' | 'jsonapi' | 'none'
82
106
  const hydraBaseRegex = userCfg.hydraBaseRegex; // RegExp|string
107
+ const includeCli = getArgs('include').flatMap((v) => v.split(',').map((s) => s.trim()).filter(Boolean));
108
+ const excludeCli = getArgs('exclude').flatMap((v) => v.split(',').map((s) => s.trim()).filter(Boolean));
109
+ const includeSchemaNames = [
110
+ ...(Array.isArray(userCfg.includeSchemaNames) ? userCfg.includeSchemaNames : []),
111
+ ...includeCli,
112
+ ];
113
+ const excludeSchemaNames = [
114
+ ...(Array.isArray(userCfg.excludeSchemaNames) ? userCfg.excludeSchemaNames : []),
115
+ ...excludeCli,
116
+ ];
117
+
118
+ if (debug) {
119
+ console.log('[meridiane models] debug', {
120
+ outDir: path.relative(workDir, outDir),
121
+ writeIndex,
122
+ itemImportPath,
123
+ requiredMode,
124
+ preset,
125
+ preferFlavor,
126
+ includeCount: includeSchemaNames.length,
127
+ excludeCount: excludeSchemaNames.length,
128
+ });
129
+ }
83
130
 
84
131
  console.log(`📥 Spec OpenAPI: ${specPathOrUrl}`);
85
132
  const spec = await readSpec(specPathOrUrl);
86
133
 
87
134
 
88
135
  console.log('🔧 Construction des interfaces…');
89
- const {models} = buildModelsFromOpenAPI(spec, { requiredMode, preferFlavor, hydraBaseRegex });
136
+ const {models} = buildModelsFromOpenAPI(spec, {
137
+ requiredMode,
138
+ preset,
139
+ preferFlavor,
140
+ hydraBaseRegex,
141
+ includeSchemaNames,
142
+ excludeSchemaNames,
143
+ });
90
144
 
91
145
  const templatesDir = path.join(__dirname, 'generator', 'models', 'templates');
92
146
  await ensureCleanDir(outDir);
@@ -1,8 +1,19 @@
1
1
  import { sanitizeTypeName, quoteKeyIfNeeded } from './utils.js';
2
- import { filterSchemaNames, mergeAllOf, isObject } from './schema-utils.js';
2
+ import { filterSchemaNames, applySchemaNameFilters, mergeAllOf, isObject } from './schema-utils.js';
3
3
  import { friendlyName } from './naming.js';
4
4
  import { tsTypeOf, withNullable } from './type-resolver.js';
5
5
 
6
+ /**
7
+ * @typedef {{
8
+ * requiredMode?: 'all-optional'|'spec',
9
+ * preferFlavor?: 'jsonld'|'jsonapi'|'none',
10
+ * hydraBaseRegex?: RegExp|string,
11
+ * preset?: 'all'|'native',
12
+ * includeSchemaNames?: Array<RegExp|string|((name: string) => boolean)>,
13
+ * excludeSchemaNames?: Array<RegExp|string|((name: string) => boolean)>,
14
+ * }} BuildOptions
15
+ */
16
+
6
17
  /**
7
18
  * Construit une map originalName -> sanitizedName, en assurant l’unicité
8
19
  * @param {string[]} schemaNames
@@ -53,7 +64,7 @@ function buildNameMap(schemaNames) {
53
64
  export function buildModelsFromOpenAPI(spec, options) {
54
65
  const opts = { requiredMode: 'all-optional', ...(options || {}) };
55
66
  const schemas = spec?.components?.schemas || {};
56
- const schemaNames = filterSchemaNames(schemas, opts);
67
+ const schemaNames = applySchemaNameFilters(filterSchemaNames(schemas, opts), opts);
57
68
  const nameMap = buildNameMap(schemaNames);
58
69
  const allSanitizedNames = new Set([...nameMap.values()]);
59
70
 
@@ -123,3 +123,62 @@ export function filterSchemaNames(schemas, cfg) {
123
123
  const noGroupSelected = [...variantsNoGroup.values()];
124
124
  return [...roots, ...[...grouped.values()], ...noGroupSelected].sort((a, b) => a.localeCompare(b));
125
125
  }
126
+
127
+ /**
128
+ * @param {unknown} rule
129
+ * @returns {rule is ((name: string) => boolean)}
130
+ */
131
+ function isRuleFn(rule) {
132
+ return typeof rule === 'function';
133
+ }
134
+
135
+ /**
136
+ * @param {string} name
137
+ * @param {RegExp|string|((name: string) => boolean)} rule
138
+ */
139
+ function matchesRule(name, rule) {
140
+ if (rule instanceof RegExp) return rule.test(name);
141
+ if (isRuleFn(rule)) return !!rule(name);
142
+ if (typeof rule === 'string' && rule.length) return name.includes(rule);
143
+ return false;
144
+ }
145
+
146
+ /**
147
+ * Applique un preset de génération et/ou des règles include/exclude.
148
+ *
149
+ * - `preset: "all"`: ne filtre rien (comportement par défaut)
150
+ * - `preset: "native"`: retire les schémas "techniques" (Hydra*, jsonMergePatch…)
151
+ *
152
+ * Les règles `includeSchemaNames` / `excludeSchemaNames` s'appliquent sur les noms
153
+ * de schémas OpenAPI (ex: "User-user.read", "Conversation.jsonMergePatch", …).
154
+ *
155
+ * @param {string[]} schemaNames
156
+ * @param {{
157
+ * preset?: 'all'|'native',
158
+ * includeSchemaNames?: Array<RegExp|string|((name: string) => boolean)>,
159
+ * excludeSchemaNames?: Array<RegExp|string|((name: string) => boolean)>,
160
+ * }} [cfg]
161
+ */
162
+ export function applySchemaNameFilters(schemaNames, cfg) {
163
+ const preset = cfg?.preset ?? 'all';
164
+
165
+ /** @type {Array<RegExp|string|((name: string) => boolean)>} */
166
+ const include = Array.isArray(cfg?.includeSchemaNames) ? cfg.includeSchemaNames : [];
167
+ /** @type {Array<RegExp|string|((name: string) => boolean)>} */
168
+ const exclude = Array.isArray(cfg?.excludeSchemaNames) ? [...cfg.excludeSchemaNames] : [];
169
+
170
+ if (preset === 'native') {
171
+ exclude.push(/^Hydra/i);
172
+ exclude.push(/jsonMergePatch/i);
173
+ exclude.push(/\.jsonld\b/i);
174
+ exclude.push(/\.jsonapi\b/i);
175
+ }
176
+
177
+ const hasInclude = include.length > 0;
178
+
179
+ return schemaNames.filter((name) => {
180
+ if (hasInclude && !include.some((r) => matchesRule(name, r))) return false;
181
+ if (exclude.some((r) => matchesRule(name, r))) return false;
182
+ return true;
183
+ });
184
+ }
@@ -33,8 +33,14 @@ 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 dotTail = original.includes('.') ? original.split('.').pop() : undefined;
37
+ const keepDottedName = typeof dotTail === 'string'
38
+ && ['jsonmergepatch', 'jsonpatch', 'mergepatch', 'jsonld', 'jsonapi'].includes(dotTail.toLowerCase());
39
+
40
+ // "Namespace-like" dotted names (ex: Workflow.WorkflowInput) default to the tail segment,
41
+ // but keep the full name for known format suffixes (ex: Conversation.jsonMergePatch) to avoid collisions.
42
+ const base = (dotTail && !keepDottedName) ? dotTail : original;
43
+ const tokens = String(base).split(/[^A-Za-z0-9]+/);
38
44
  let name = pascalCase(tokens) || 'Model';
39
45
  if (/^[0-9]/.test(name)) name = '_' + name;
40
46
  if (RESERVED.has(name)) name = name + 'Model';