generate-ui-cli 2.1.7 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +216 -27
- package/dist/commands/angular.js +535 -40
- package/dist/commands/generate.js +350 -20
- package/dist/commands/login.js +131 -42
- package/dist/commands/merge.js +201 -0
- package/dist/generate-ui/routes.gen.js +4 -0
- package/dist/generators/angular/feature.generator.js +3183 -583
- package/dist/generators/angular/menu.generator.js +165 -0
- package/dist/generators/angular/routes.generator.js +8 -3
- package/dist/generators/screen.generator.js +100 -5
- package/dist/generators/screen.merge.js +70 -15
- package/dist/index.js +88 -12
- package/dist/license/guard.js +2 -1
- package/dist/license/permissions.js +63 -9
- package/dist/license/token.js +38 -3
- package/dist/postinstall.js +47 -0
- package/dist/runtime/config.js +4 -0
- package/dist/runtime/logger.js +29 -0
- package/dist/runtime/project-config.js +64 -0
- package/package.json +1 -1
|
@@ -10,26 +10,40 @@ const load_openapi_1 = require("../openapi/load-openapi");
|
|
|
10
10
|
const screen_generator_1 = require("../generators/screen.generator");
|
|
11
11
|
const screen_merge_1 = require("../generators/screen.merge");
|
|
12
12
|
const permissions_1 = require("../license/permissions");
|
|
13
|
-
const device_1 = require("../license/device");
|
|
14
13
|
const telemetry_1 = require("../telemetry");
|
|
15
14
|
const user_config_1 = require("../runtime/user-config");
|
|
15
|
+
const logger_1 = require("../runtime/logger");
|
|
16
|
+
const project_config_1 = require("../runtime/project-config");
|
|
16
17
|
async function generate(options) {
|
|
17
18
|
void (0, telemetry_1.trackCommand)('generate', options.telemetryEnabled);
|
|
19
|
+
const projectConfig = (0, project_config_1.findProjectConfig)(process.cwd());
|
|
20
|
+
const configuredOpenApi = (0, project_config_1.pickConfiguredPath)(projectConfig.config, 'openapi');
|
|
21
|
+
const configuredOutput = (0, project_config_1.pickConfiguredPath)(projectConfig.config, 'output') ??
|
|
22
|
+
(0, project_config_1.pickConfiguredPath)(projectConfig.config, 'schemas');
|
|
23
|
+
const openApiPath = (0, project_config_1.resolveOptionalPath)(options.openapi, configuredOpenApi, projectConfig.configPath);
|
|
24
|
+
if (!openApiPath) {
|
|
25
|
+
throw new Error('Missing OpenAPI file.\n' +
|
|
26
|
+
'Use --openapi <path> or set "openapi" (or "paths.openapi") in generateui-config.json.');
|
|
27
|
+
}
|
|
18
28
|
/**
|
|
19
29
|
* Caminho absoluto do OpenAPI (YAML)
|
|
20
30
|
* Ex: /Users/.../generateui-playground/realWorldOpenApi.yaml
|
|
21
31
|
*/
|
|
22
|
-
|
|
32
|
+
(0, logger_1.logStep)(`OpenAPI: ${openApiPath}`);
|
|
23
33
|
/**
|
|
24
34
|
* Raiz do playground (onde está o YAML)
|
|
25
35
|
*/
|
|
26
36
|
const projectRoot = path_1.default.dirname(openApiPath);
|
|
37
|
+
(0, logger_1.logDebug)(`Project root: ${projectRoot}`);
|
|
27
38
|
/**
|
|
28
39
|
* Onde o Angular consome os arquivos
|
|
29
40
|
*/
|
|
30
|
-
const generateUiRoot = resolveGenerateUiRoot(projectRoot, options.output);
|
|
41
|
+
const generateUiRoot = resolveGenerateUiRoot(projectRoot, (0, project_config_1.resolveOptionalPath)(options.output, configuredOutput, projectConfig.configPath) ?? undefined);
|
|
42
|
+
(0, logger_1.logStep)(`Schemas output: ${generateUiRoot}`);
|
|
31
43
|
const generatedDir = path_1.default.join(generateUiRoot, 'generated');
|
|
32
44
|
const overlaysDir = path_1.default.join(generateUiRoot, 'overlays');
|
|
45
|
+
(0, logger_1.logDebug)(`Generated dir: ${generatedDir}`);
|
|
46
|
+
(0, logger_1.logDebug)(`Overlays dir: ${overlaysDir}`);
|
|
33
47
|
(0, user_config_1.updateUserConfig)(config => ({
|
|
34
48
|
...config,
|
|
35
49
|
lastSchemasPath: generateUiRoot
|
|
@@ -41,23 +55,26 @@ async function generate(options) {
|
|
|
41
55
|
*/
|
|
42
56
|
const routes = [];
|
|
43
57
|
const usedOperationIds = new Set();
|
|
58
|
+
const resourceMap = new Map();
|
|
59
|
+
const screenByOpId = new Map();
|
|
60
|
+
const viewDefaults = [];
|
|
44
61
|
const permissions = await (0, permissions_1.getPermissions)();
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
'O plano Dev libera gerações ilimitadas, regeneração segura e UI inteligente.\n' +
|
|
50
|
-
'👉 Execute `generate-ui login` para continuar.');
|
|
62
|
+
const subscriptionReason = normalizeSubscriptionReason(permissions.subscription.reason);
|
|
63
|
+
(0, logger_1.logDebug)(`License: status=${permissions.subscription.status}, overrides=${permissions.features.uiOverrides}, safeRegen=${permissions.features.safeRegeneration}, intelligent=${permissions.features.intelligentGeneration}`);
|
|
64
|
+
if (subscriptionReason) {
|
|
65
|
+
console.log(`ℹ Subscription: ${subscriptionReason}`);
|
|
51
66
|
}
|
|
52
67
|
/**
|
|
53
68
|
* Parse do OpenAPI (já com $refs resolvidos)
|
|
54
69
|
*/
|
|
55
70
|
const api = await (0, load_openapi_1.loadOpenApi)(openApiPath);
|
|
56
71
|
const paths = api.paths ?? {};
|
|
72
|
+
(0, logger_1.logDebug)(`OpenAPI paths: ${Object.keys(paths).length}`);
|
|
57
73
|
/**
|
|
58
74
|
* Itera por todos os endpoints
|
|
59
75
|
*/
|
|
60
76
|
const operationIds = new Set();
|
|
77
|
+
const operationFileNames = new Set();
|
|
61
78
|
for (const [pathKey, pathItem] of Object.entries(paths)) {
|
|
62
79
|
for (const [method, rawOp] of Object.entries(pathItem)) {
|
|
63
80
|
const op = rawOp;
|
|
@@ -66,6 +83,8 @@ async function generate(options) {
|
|
|
66
83
|
const operationId = op.operationId ||
|
|
67
84
|
buildOperationId(method.toLowerCase(), pathKey, usedOperationIds);
|
|
68
85
|
operationIds.add(operationId);
|
|
86
|
+
operationFileNames.add(toSafeFileName(operationId));
|
|
87
|
+
recordResourceOp(resourceMap, pathKey, method.toLowerCase(), operationId, op);
|
|
69
88
|
const endpoint = {
|
|
70
89
|
operationId,
|
|
71
90
|
path: pathKey,
|
|
@@ -77,7 +96,8 @@ async function generate(options) {
|
|
|
77
96
|
* Gera o ScreenSchema completo
|
|
78
97
|
*/
|
|
79
98
|
const screenSchema = (0, screen_generator_1.generateScreen)(endpoint, api);
|
|
80
|
-
|
|
99
|
+
screenByOpId.set(operationId, screenSchema);
|
|
100
|
+
const fileName = `${toSafeFileName(operationId)}.screen.json`;
|
|
81
101
|
/**
|
|
82
102
|
* 1️⃣ generated → SEMPRE sobrescrito (base técnica)
|
|
83
103
|
*/
|
|
@@ -117,16 +137,124 @@ async function generate(options) {
|
|
|
117
137
|
*/
|
|
118
138
|
routes.push({
|
|
119
139
|
path: operationId,
|
|
120
|
-
operationId
|
|
140
|
+
operationId,
|
|
141
|
+
label: toLabel(screenSchema.entity
|
|
142
|
+
? String(screenSchema.entity)
|
|
143
|
+
: operationId),
|
|
144
|
+
group: inferRouteGroup(op, pathKey)
|
|
145
|
+
});
|
|
146
|
+
viewDefaults.push({
|
|
147
|
+
key: operationId,
|
|
148
|
+
view: 'list'
|
|
121
149
|
});
|
|
122
150
|
console.log(`✔ Generated ${operationId}`);
|
|
123
151
|
}
|
|
124
152
|
}
|
|
153
|
+
const canOverride = permissions.features.uiOverrides;
|
|
154
|
+
const canRegenerateSafely = permissions.features.safeRegeneration;
|
|
155
|
+
const intelligentEnabled = Boolean(permissions.features.intelligentGeneration);
|
|
156
|
+
if (intelligentEnabled) {
|
|
157
|
+
const adminSchemas = buildAdminSchemas(resourceMap, usedOperationIds, screenByOpId);
|
|
158
|
+
for (const admin of adminSchemas) {
|
|
159
|
+
const fileName = `${toSafeFileName(admin.api.operationId)}.screen.json`;
|
|
160
|
+
const generatedPath = path_1.default.join(generatedDir, fileName);
|
|
161
|
+
fs_1.default.writeFileSync(generatedPath, JSON.stringify(admin, null, 2));
|
|
162
|
+
const overlayPath = path_1.default.join(overlaysDir, fileName);
|
|
163
|
+
if (canOverride && canRegenerateSafely) {
|
|
164
|
+
const overlay = fs_1.default.existsSync(overlayPath)
|
|
165
|
+
? JSON.parse(fs_1.default.readFileSync(overlayPath, 'utf-8'))
|
|
166
|
+
: null;
|
|
167
|
+
const merged = (0, screen_merge_1.mergeScreen)(admin, overlay, null, {
|
|
168
|
+
openapiVersion: api?.info?.version || 'unknown',
|
|
169
|
+
debug: options.debug
|
|
170
|
+
});
|
|
171
|
+
fs_1.default.writeFileSync(overlayPath, JSON.stringify(merged.screen, null, 2));
|
|
172
|
+
}
|
|
173
|
+
else if (!fs_1.default.existsSync(overlayPath)) {
|
|
174
|
+
fs_1.default.writeFileSync(overlayPath, JSON.stringify(admin, null, 2));
|
|
175
|
+
}
|
|
176
|
+
routes.push({
|
|
177
|
+
path: admin.api.operationId,
|
|
178
|
+
operationId: admin.api.operationId,
|
|
179
|
+
label: admin.meta?.intelligent?.label,
|
|
180
|
+
group: admin.meta?.intelligent?.group ?? null
|
|
181
|
+
});
|
|
182
|
+
viewDefaults.push({
|
|
183
|
+
key: admin.api.operationId,
|
|
184
|
+
view: 'cards'
|
|
185
|
+
});
|
|
186
|
+
if (admin.meta?.intelligent?.listOperationId) {
|
|
187
|
+
viewDefaults.push({
|
|
188
|
+
key: admin.meta.intelligent.listOperationId,
|
|
189
|
+
view: 'list'
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
operationIds.add(admin.api.operationId);
|
|
193
|
+
operationFileNames.add(toSafeFileName(admin.api.operationId));
|
|
194
|
+
console.log(`✨ Generated ${admin.api.operationId}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
(0, logger_1.logStep)(`Screens generated: ${routes.length}`);
|
|
125
198
|
/**
|
|
126
199
|
* 4️⃣ Gera arquivo de rotas
|
|
127
200
|
*/
|
|
128
201
|
const routesPath = path_1.default.join(generateUiRoot, 'routes.json');
|
|
129
202
|
fs_1.default.writeFileSync(routesPath, JSON.stringify(routes, null, 2));
|
|
203
|
+
(0, logger_1.logDebug)(`Routes written: ${routesPath}`);
|
|
204
|
+
/**
|
|
205
|
+
* 4.3️⃣ Gera generateui-config.json (não sobrescreve)
|
|
206
|
+
*/
|
|
207
|
+
const configPath = path_1.default.join(generateUiRoot, '..', '..', 'generateui-config.json');
|
|
208
|
+
let configPayload = null;
|
|
209
|
+
if (!fs_1.default.existsSync(configPath)) {
|
|
210
|
+
configPayload = {
|
|
211
|
+
appTitle: 'Generate UI',
|
|
212
|
+
defaultRoute: '',
|
|
213
|
+
menu: {
|
|
214
|
+
autoInject: true
|
|
215
|
+
},
|
|
216
|
+
views: {}
|
|
217
|
+
};
|
|
218
|
+
fs_1.default.writeFileSync(configPath, JSON.stringify(configPayload, null, 2));
|
|
219
|
+
(0, logger_1.logDebug)(`Config created: ${configPath}`);
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
try {
|
|
223
|
+
configPayload = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
configPayload = null;
|
|
227
|
+
}
|
|
228
|
+
(0, logger_1.logDebug)(`Config found: ${configPath}`);
|
|
229
|
+
}
|
|
230
|
+
if (configPayload && viewDefaults.length) {
|
|
231
|
+
configPayload.views = configPayload.views || {};
|
|
232
|
+
for (const entry of viewDefaults) {
|
|
233
|
+
if (!configPayload.views[entry.key]) {
|
|
234
|
+
configPayload.views[entry.key] = entry.view;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
fs_1.default.writeFileSync(configPath, JSON.stringify(configPayload, null, 2));
|
|
238
|
+
(0, logger_1.logDebug)(`Config views updated: ${configPath}`);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* 4.1️⃣ Gera menu inicial (override possível via menu.overrides.json)
|
|
242
|
+
*/
|
|
243
|
+
const menuPath = path_1.default.join(generateUiRoot, 'menu.json');
|
|
244
|
+
const menu = buildMenuFromRoutes(routes);
|
|
245
|
+
fs_1.default.writeFileSync(menuPath, JSON.stringify(menu, null, 2));
|
|
246
|
+
(0, logger_1.logDebug)(`Menu written: ${menuPath}`);
|
|
247
|
+
/**
|
|
248
|
+
* 4.2️⃣ Gera menu.overrides.json (não sobrescreve)
|
|
249
|
+
*/
|
|
250
|
+
const menuOverridesPath = path_1.default.join(generateUiRoot, 'menu.overrides.json');
|
|
251
|
+
if (!fs_1.default.existsSync(menuOverridesPath)) {
|
|
252
|
+
fs_1.default.writeFileSync(menuOverridesPath, JSON.stringify(menu, null, 2));
|
|
253
|
+
(0, logger_1.logDebug)(`Menu overrides created: ${menuOverridesPath}`);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
(0, logger_1.logDebug)(`Menu overrides found: ${menuOverridesPath}`);
|
|
257
|
+
}
|
|
130
258
|
/**
|
|
131
259
|
* 5️⃣ Remove overlays órfãos (endpoint removido)
|
|
132
260
|
*/
|
|
@@ -135,30 +263,44 @@ async function generate(options) {
|
|
|
135
263
|
.filter(file => file.endsWith('.screen.json'));
|
|
136
264
|
for (const file of overlayFiles) {
|
|
137
265
|
const opId = file.replace(/\.screen\.json$/, '');
|
|
138
|
-
if (!
|
|
266
|
+
if (!operationFileNames.has(opId)) {
|
|
139
267
|
fs_1.default.rmSync(path_1.default.join(overlaysDir, file));
|
|
140
268
|
if (options.debug) {
|
|
141
269
|
console.log(`✖ Removed overlay ${opId}`);
|
|
142
270
|
}
|
|
143
271
|
}
|
|
144
272
|
}
|
|
145
|
-
if (permissions.features.maxGenerations > -1) {
|
|
146
|
-
(0, device_1.incrementFreeGeneration)();
|
|
147
|
-
}
|
|
148
273
|
console.log('✔ Routes generated');
|
|
274
|
+
console.log('');
|
|
275
|
+
console.log('🎉 Next steps');
|
|
276
|
+
console.log(' 1) Default full flow:');
|
|
277
|
+
console.log(' generate-ui generate');
|
|
278
|
+
console.log(' 2) Advanced (schemas only):');
|
|
279
|
+
console.log(' generate-ui schema');
|
|
280
|
+
console.log(' 3) Advanced (Angular only):');
|
|
281
|
+
console.log(' generate-ui angular');
|
|
282
|
+
console.log(' 4) Customize screens in generate-ui/overlays/');
|
|
283
|
+
console.log(' 5) Customize menu in generate-ui/menu.overrides.json (created once, never overwritten)');
|
|
284
|
+
console.log(' 6) Edit generateui-config.json to set appTitle/defaultRoute/menu.autoInject/views');
|
|
285
|
+
console.log('');
|
|
286
|
+
(0, logger_1.logTip)('Run with --dev to see detailed logs and file paths.');
|
|
287
|
+
}
|
|
288
|
+
function normalizeSubscriptionReason(reason) {
|
|
289
|
+
if (typeof reason !== 'string')
|
|
290
|
+
return '';
|
|
291
|
+
const trimmed = reason.trim();
|
|
292
|
+
return trimmed;
|
|
149
293
|
}
|
|
150
294
|
function resolveGenerateUiRoot(projectRoot, output) {
|
|
151
295
|
if (output) {
|
|
152
|
-
return path_1.default.
|
|
296
|
+
return path_1.default.isAbsolute(output)
|
|
297
|
+
? output
|
|
298
|
+
: path_1.default.resolve(process.cwd(), output);
|
|
153
299
|
}
|
|
154
300
|
const srcRoot = path_1.default.join(projectRoot, 'src');
|
|
155
301
|
if (fs_1.default.existsSync(srcRoot)) {
|
|
156
302
|
return path_1.default.join(srcRoot, 'generate-ui');
|
|
157
303
|
}
|
|
158
|
-
const frontendSrcRoot = path_1.default.join(projectRoot, 'frontend', 'src');
|
|
159
|
-
if (fs_1.default.existsSync(frontendSrcRoot)) {
|
|
160
|
-
return path_1.default.join(frontendSrcRoot, 'generate-ui');
|
|
161
|
-
}
|
|
162
304
|
return path_1.default.join(projectRoot, 'generate-ui');
|
|
163
305
|
}
|
|
164
306
|
function mergeParameters(pathParams, opParams) {
|
|
@@ -199,6 +341,194 @@ function buildOperationId(method, pathKey, usedOperationIds) {
|
|
|
199
341
|
usedOperationIds.add(candidate);
|
|
200
342
|
return candidate;
|
|
201
343
|
}
|
|
344
|
+
function inferRouteGroup(op, pathKey) {
|
|
345
|
+
const tag = Array.isArray(op?.tags) && op.tags.length
|
|
346
|
+
? String(op.tags[0]).trim()
|
|
347
|
+
: '';
|
|
348
|
+
if (tag)
|
|
349
|
+
return tag;
|
|
350
|
+
const segment = String(pathKey || '')
|
|
351
|
+
.split('/')
|
|
352
|
+
.map(part => part.trim())
|
|
353
|
+
.filter(Boolean)
|
|
354
|
+
.find(part => !part.startsWith('{') && !part.endsWith('}'));
|
|
355
|
+
return segment ? segment : null;
|
|
356
|
+
}
|
|
357
|
+
function recordResourceOp(map, pathKey, method, operationId, op) {
|
|
358
|
+
const isItemPath = /\/{[^}]+}$/.test(pathKey);
|
|
359
|
+
const basePath = isItemPath
|
|
360
|
+
? pathKey.replace(/\/{[^}]+}$/, '')
|
|
361
|
+
: pathKey;
|
|
362
|
+
const paramMatch = isItemPath
|
|
363
|
+
? pathKey.match(/\/{([^}]+)}$/)
|
|
364
|
+
: null;
|
|
365
|
+
const idParam = paramMatch ? paramMatch[1] : null;
|
|
366
|
+
const tag = Array.isArray(op?.tags) && op.tags.length
|
|
367
|
+
? String(op.tags[0]).trim()
|
|
368
|
+
: undefined;
|
|
369
|
+
const entry = map.get(basePath) || {
|
|
370
|
+
basePath
|
|
371
|
+
};
|
|
372
|
+
if (tag && !entry.tag)
|
|
373
|
+
entry.tag = tag;
|
|
374
|
+
if (idParam && !entry.idParam)
|
|
375
|
+
entry.idParam = idParam;
|
|
376
|
+
const payload = {
|
|
377
|
+
operationId,
|
|
378
|
+
method,
|
|
379
|
+
path: pathKey,
|
|
380
|
+
tag
|
|
381
|
+
};
|
|
382
|
+
if (!isItemPath && method === 'get')
|
|
383
|
+
entry.list = payload;
|
|
384
|
+
if (isItemPath && method === 'get')
|
|
385
|
+
entry.detail = payload;
|
|
386
|
+
if (isItemPath && (method === 'put' || method === 'patch')) {
|
|
387
|
+
entry.update = payload;
|
|
388
|
+
}
|
|
389
|
+
if (isItemPath && method === 'delete')
|
|
390
|
+
entry.remove = payload;
|
|
391
|
+
map.set(basePath, entry);
|
|
392
|
+
}
|
|
393
|
+
function buildAdminSchemas(resources, usedOperationIds, screenByOpId) {
|
|
394
|
+
const adminSchemas = [];
|
|
395
|
+
for (const resource of resources.values()) {
|
|
396
|
+
if (!resource.list)
|
|
397
|
+
continue;
|
|
398
|
+
const entity = inferEntityName(resource.basePath);
|
|
399
|
+
const baseId = toPascalCase(entity);
|
|
400
|
+
const baseOpId = `${baseId}Admin`;
|
|
401
|
+
let operationId = baseOpId;
|
|
402
|
+
let index = 2;
|
|
403
|
+
while (usedOperationIds.has(operationId)) {
|
|
404
|
+
operationId = `${baseOpId}${index}`;
|
|
405
|
+
index += 1;
|
|
406
|
+
}
|
|
407
|
+
usedOperationIds.add(operationId);
|
|
408
|
+
const label = `${toLabel(entity)} Admin`;
|
|
409
|
+
const group = resource.tag ? resource.tag : inferGroup(resource.basePath);
|
|
410
|
+
const listSchema = screenByOpId.get(resource.list.operationId);
|
|
411
|
+
const columns = listSchema?.data?.table?.columns &&
|
|
412
|
+
Array.isArray(listSchema.data.table.columns)
|
|
413
|
+
? listSchema.data.table.columns
|
|
414
|
+
: [];
|
|
415
|
+
const responseFormat = listSchema?.response?.format === 'cards' ||
|
|
416
|
+
listSchema?.response?.format === 'raw' ||
|
|
417
|
+
listSchema?.response?.format === 'table'
|
|
418
|
+
? listSchema.response.format
|
|
419
|
+
: columns.length > 0
|
|
420
|
+
? 'table'
|
|
421
|
+
: null;
|
|
422
|
+
adminSchemas.push({
|
|
423
|
+
meta: {
|
|
424
|
+
intelligent: {
|
|
425
|
+
kind: 'adminList',
|
|
426
|
+
label,
|
|
427
|
+
group,
|
|
428
|
+
listOperationId: resource.list.operationId,
|
|
429
|
+
detailOperationId: resource.detail?.operationId ?? null,
|
|
430
|
+
updateOperationId: resource.update?.operationId ?? null,
|
|
431
|
+
deleteOperationId: resource.remove?.operationId ?? null,
|
|
432
|
+
idParam: resource.idParam ?? null
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
entity: toLabel(entity),
|
|
436
|
+
description: 'Smart admin list generated from collection endpoints.',
|
|
437
|
+
data: {
|
|
438
|
+
table: {
|
|
439
|
+
columns
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
response: responseFormat
|
|
443
|
+
? { format: responseFormat }
|
|
444
|
+
: undefined,
|
|
445
|
+
api: {
|
|
446
|
+
operationId,
|
|
447
|
+
method: 'get',
|
|
448
|
+
endpoint: resource.list.path
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
return adminSchemas;
|
|
453
|
+
}
|
|
454
|
+
function inferEntityName(pathKey) {
|
|
455
|
+
const segments = String(pathKey || '')
|
|
456
|
+
.split('/')
|
|
457
|
+
.filter(Boolean);
|
|
458
|
+
if (!segments.length)
|
|
459
|
+
return 'Resource';
|
|
460
|
+
const last = segments[segments.length - 1];
|
|
461
|
+
return last.replace(/[^a-zA-Z0-9]+/g, ' ');
|
|
462
|
+
}
|
|
463
|
+
function inferGroup(pathKey) {
|
|
464
|
+
const segment = String(pathKey || '')
|
|
465
|
+
.split('/')
|
|
466
|
+
.map(part => part.trim())
|
|
467
|
+
.filter(Boolean)[0];
|
|
468
|
+
return segment ? segment : null;
|
|
469
|
+
}
|
|
470
|
+
function buildMenuFromRoutes(routes) {
|
|
471
|
+
const groups = [];
|
|
472
|
+
const ungrouped = [];
|
|
473
|
+
const groupMap = new Map();
|
|
474
|
+
for (const route of routes) {
|
|
475
|
+
const item = {
|
|
476
|
+
id: route.operationId,
|
|
477
|
+
label: toLabel(route.label || route.operationId),
|
|
478
|
+
route: route.path
|
|
479
|
+
};
|
|
480
|
+
const rawGroup = route.group ? String(route.group) : '';
|
|
481
|
+
if (!rawGroup) {
|
|
482
|
+
ungrouped.push(item);
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
const groupId = toKebab(rawGroup);
|
|
486
|
+
let group = groupMap.get(groupId);
|
|
487
|
+
if (!group) {
|
|
488
|
+
group = {
|
|
489
|
+
id: groupId,
|
|
490
|
+
label: toLabel(rawGroup),
|
|
491
|
+
items: []
|
|
492
|
+
};
|
|
493
|
+
groupMap.set(groupId, group);
|
|
494
|
+
groups.push(group);
|
|
495
|
+
}
|
|
496
|
+
group.items.push(item);
|
|
497
|
+
}
|
|
498
|
+
return {
|
|
499
|
+
groups,
|
|
500
|
+
ungrouped
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
function toKebab(value) {
|
|
504
|
+
return String(value)
|
|
505
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
506
|
+
.replace(/[_\s]+/g, '-')
|
|
507
|
+
.toLowerCase();
|
|
508
|
+
}
|
|
509
|
+
function toLabel(value) {
|
|
510
|
+
return stripDiacritics(String(value))
|
|
511
|
+
.replace(/[_-]/g, ' ')
|
|
512
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
513
|
+
.replace(/\b\w/g, char => char.toUpperCase());
|
|
514
|
+
}
|
|
515
|
+
function toSafeFileName(value) {
|
|
516
|
+
return String(value)
|
|
517
|
+
.replace(/[\\/]/g, '-')
|
|
518
|
+
.replace(/\s+/g, '-');
|
|
519
|
+
}
|
|
520
|
+
function stripDiacritics(value) {
|
|
521
|
+
return value
|
|
522
|
+
.normalize('NFD')
|
|
523
|
+
.replace(/[\u0300-\u036f]/g, '');
|
|
524
|
+
}
|
|
525
|
+
function toPascalCase(value) {
|
|
526
|
+
return String(value)
|
|
527
|
+
.split(/[^a-zA-Z0-9]+/)
|
|
528
|
+
.filter(Boolean)
|
|
529
|
+
.map(part => part[0].toUpperCase() + part.slice(1))
|
|
530
|
+
.join('');
|
|
531
|
+
}
|
|
202
532
|
function httpVerbToPrefix(verb) {
|
|
203
533
|
switch (verb) {
|
|
204
534
|
case 'get':
|