@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.
- package/README.md +42 -2
- package/cli.js +27 -1
- package/models.config.example.js +15 -0
- package/package.json +1 -3
- package/templates/_lib_template/src/lib/bridge/rest/api-platform.adapter.ts +48 -47
- package/templates/_lib_template/src/lib/bridge/rest/query-builder.ts +29 -29
- package/templates/_lib_template/src/lib/bridge/sse/eventsource-wrapper.ts +50 -54
- package/templates/_lib_template/src/lib/bridge/sse/mercure-topic.mapper.ts +48 -0
- package/templates/_lib_template/src/lib/bridge/sse/mercure-url.builder.ts +7 -7
- package/templates/_lib_template/src/lib/bridge/sse/mercure.adapter.ts +104 -49
- package/templates/_lib_template/src/lib/bridge/sse/ref-count-topic.registry.ts +23 -21
- package/templates/_lib_template/src/lib/bridge.types.ts +33 -0
- package/templates/_lib_template/src/lib/facades/bridge.facade.ts +116 -0
- package/templates/_lib_template/src/lib/facades/facade.factory.ts +21 -19
- package/templates/_lib_template/src/lib/facades/facade.interface.ts +13 -18
- package/templates/_lib_template/src/lib/facades/resource.facade.ts +42 -56
- package/templates/_lib_template/src/lib/interceptors/bridge-debug.interceptor.ts +32 -0
- package/templates/_lib_template/src/lib/interceptors/bridge-defaults.interceptor.ts +53 -0
- package/templates/_lib_template/src/lib/interceptors/content-type.interceptor.ts +36 -37
- package/templates/_lib_template/src/lib/ports/realtime.port.ts +5 -3
- package/templates/_lib_template/src/lib/ports/resource-repository.port.ts +14 -15
- package/templates/_lib_template/src/lib/provide-bridge.ts +122 -19
- package/templates/_lib_template/src/lib/tokens.ts +14 -1
- package/templates/_lib_template/src/lib/utils/url.ts +15 -0
- package/templates/_lib_template/src/public-api.ts +3 -2
- package/tools/dev-bridge.js +138 -0
- package/tools/generate-lib.js +60 -45
- package/tools/generate-models.js +73 -19
- package/tools/generator/models/openapi-to-models.js +13 -2
- package/tools/generator/models/schema-utils.js +59 -0
- package/tools/generator/models/utils.js +8 -2
- package/tools/init.js +148 -0
- package/tools/sandbox-bridge.js +77 -0
- package/tools/utils/cli-args.js +41 -0
- package/tools/utils/dotenv.js +55 -0
- package/templates/_lib_template/src/lib/bridge/credentials.policy.ts +0 -9
- 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
|
+
});
|
package/tools/generate-lib.js
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import fs from
|
|
3
|
-
import
|
|
4
|
-
import
|
|
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
|
|
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, {
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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.
|
|
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
|
-
|
|
56
|
-
const libPkg = await
|
|
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
|
|
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
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
92
|
+
defaultConfiguration: 'production',
|
|
88
93
|
},
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
});
|
package/tools/generate-models.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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] [--
|
|
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 =
|
|
76
|
-
|
|
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, {
|
|
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
|
|
37
|
-
const
|
|
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';
|