springnext 0.0.1 → 0.0.3
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/bin/cli.js +1537 -0
- package/dist/errors.utils.d.ts +37 -0
- package/dist/index.cjs +510 -4
- package/dist/index.d.ts +4 -1
- package/dist/index.js +511 -2
- package/dist/store/index.d.ts +3 -0
- package/dist/store/store.ram.spec.d.ts +1 -0
- package/dist/store/store.ram.utils.d.ts +969 -0
- package/dist/store/store.shared-models.utils.d.ts +74 -0
- package/dist/store/store.zod.utils.d.ts +581 -0
- package/dist/value-objects/index.d.ts +1 -0
- package/dist/value-objects/pagination.value-object.d.ts +19 -0
- package/dist/zod-controller.spec.d.ts +1 -0
- package/dist/zod-controller.utils.d.ts +126 -0
- package/dist/zod-module.spec.d.ts +1 -0
- package/dist/zod-module.utils.d.ts +29 -0
- package/package.json +17 -3
package/bin/cli.js
ADDED
|
@@ -0,0 +1,1537 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
|
|
5
|
+
var args = process.argv.slice(2);
|
|
6
|
+
|
|
7
|
+
var [command, entityName, ...options] = args;
|
|
8
|
+
|
|
9
|
+
function insertAfterLineInFile(filePath, targetLine, newLine) {
|
|
10
|
+
let content = fs.readFileSync(filePath, 'utf8');
|
|
11
|
+
|
|
12
|
+
const lines = content.split('\n');
|
|
13
|
+
const index = lines.findIndex(line => line.includes(targetLine));
|
|
14
|
+
|
|
15
|
+
if (index !== -1) {
|
|
16
|
+
lines.splice(index + 1, 0, newLine);
|
|
17
|
+
fs.writeFileSync(filePath, lines.join('\n'), 'utf8');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function insertBeforeLineInFile(filePath, targetLine, newLine, before = true) {
|
|
22
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
23
|
+
const lines = content.split('\n');
|
|
24
|
+
|
|
25
|
+
const index = lines.findIndex(line => line.includes(targetLine));
|
|
26
|
+
|
|
27
|
+
if (index !== -1) {
|
|
28
|
+
lines.splice(before ? index - 1 : index, 0, newLine);
|
|
29
|
+
fs.writeFileSync(filePath, lines.join('\n'), 'utf8');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function camelizeVariants(str) {
|
|
34
|
+
if (!str.includes('-')) {
|
|
35
|
+
return [str, str.substring(0, 1).toUpperCase() + str.substring(1)]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const words = str.split("-");
|
|
39
|
+
|
|
40
|
+
const lowerCamel = words
|
|
41
|
+
.map((word, index) =>
|
|
42
|
+
index === 0 ? word.toLowerCase() : word[0].toUpperCase() + word.slice(1).toLowerCase()
|
|
43
|
+
)
|
|
44
|
+
.join("");
|
|
45
|
+
|
|
46
|
+
const upperCamel = words
|
|
47
|
+
.map(word => word[0].toUpperCase() + word.slice(1).toLowerCase())
|
|
48
|
+
.join("");
|
|
49
|
+
|
|
50
|
+
return [lowerCamel, upperCamel];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function findProjectRoot(startDir = process.cwd()) {
|
|
54
|
+
let dir = startDir;
|
|
55
|
+
while (dir !== path.parse(dir).root) {
|
|
56
|
+
if (fs.existsSync(path.join(dir, "package.json"))) {
|
|
57
|
+
return dir;
|
|
58
|
+
}
|
|
59
|
+
dir = path.dirname(dir);
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function loadConfig() {
|
|
65
|
+
const projectRoot = findProjectRoot();
|
|
66
|
+
if (!projectRoot) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const configPath = path.join(projectRoot, "nzmt.config.json");
|
|
71
|
+
|
|
72
|
+
if (!fs.existsSync(configPath)) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const rawData = fs.readFileSync(configPath, "utf-8");
|
|
78
|
+
const config = JSON.parse(rawData);
|
|
79
|
+
return config;
|
|
80
|
+
} catch (err) {
|
|
81
|
+
throw err;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const config = loadConfig();
|
|
86
|
+
|
|
87
|
+
function createDefaultConfig() {
|
|
88
|
+
if (!config) {
|
|
89
|
+
const projectRoot = findProjectRoot()
|
|
90
|
+
if (!projectRoot) {
|
|
91
|
+
throw 'No package.json was found'
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const folders = fs.readdirSync(projectRoot, { withFileTypes: true }).filter(x => x.isDirectory).map(x => x.name)
|
|
95
|
+
const withSrcFolder = folders.includes('src')
|
|
96
|
+
const coreFolder = withSrcFolder ? './src' : '.'
|
|
97
|
+
|
|
98
|
+
const prismaClientPathOption = [entityName, ...options].filter(x => typeof x === 'string').find(x => x.startsWith('prismaClientPath:'))
|
|
99
|
+
|
|
100
|
+
let prismaClientPath = prismaClientPathOption ? prismaClientPathOption.split(':')[1] : undefined
|
|
101
|
+
|
|
102
|
+
fs.writeFileSync(path.resolve(projectRoot, 'nzmt.config.json'), JSON.stringify(prismaClientPath ? {
|
|
103
|
+
coreFolder,
|
|
104
|
+
paths: {
|
|
105
|
+
di: '/server/di',
|
|
106
|
+
stores: '/server/stores',
|
|
107
|
+
services: '/server/services',
|
|
108
|
+
providers: '/server/providers',
|
|
109
|
+
controllers: '/server/controllers',
|
|
110
|
+
infrastructure: '/server/infrastructure',
|
|
111
|
+
entities: '/domain/entities',
|
|
112
|
+
valueObjects: '/domain/value-objects',
|
|
113
|
+
sharedErrors: '/domain/errors',
|
|
114
|
+
queries: '/ui/shared/queries',
|
|
115
|
+
clientUtils: '/ui/shared/utils',
|
|
116
|
+
widgets: '/ui/widgets'
|
|
117
|
+
},
|
|
118
|
+
store: {
|
|
119
|
+
prisma: {
|
|
120
|
+
clientPath: prismaClientPath
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
services: {
|
|
124
|
+
defaultInjections: []
|
|
125
|
+
}
|
|
126
|
+
} : {
|
|
127
|
+
coreFolder,
|
|
128
|
+
paths: {
|
|
129
|
+
di: '/server/di',
|
|
130
|
+
stores: '/server/stores',
|
|
131
|
+
services: '/server/services',
|
|
132
|
+
providers: '/server/providers',
|
|
133
|
+
controllers: '/server/controllers',
|
|
134
|
+
infrastructure: '/server/infrastructure',
|
|
135
|
+
entities: '/domain/entities',
|
|
136
|
+
valueObjects: '/domain/value-objects',
|
|
137
|
+
sharedErrors: '/domain/errors',
|
|
138
|
+
queries: '/ui/shared/queries',
|
|
139
|
+
clientUtils: '/ui/shared/utils',
|
|
140
|
+
widgets: '/ui/widgets'
|
|
141
|
+
},
|
|
142
|
+
services: {
|
|
143
|
+
defaultInjections: []
|
|
144
|
+
}
|
|
145
|
+
}, null, '\t'))
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function initSharedErrors() {
|
|
150
|
+
const config = loadConfig()
|
|
151
|
+
const folder = path.resolve(process.cwd(), `${config.coreFolder}${config.paths.sharedErrors}`)
|
|
152
|
+
fs.mkdirSync(folder, { recursive: true })
|
|
153
|
+
|
|
154
|
+
fs.writeFileSync(path.resolve(folder, 'shared-error-codes.ts'), [
|
|
155
|
+
"export enum CommonErrorCodes {",
|
|
156
|
+
"\tNO_DATA_WAS_FOUND = 'COMMON___NO_DATA_WAS_FOUND'",
|
|
157
|
+
"}"
|
|
158
|
+
].join('\n'))
|
|
159
|
+
|
|
160
|
+
fs.writeFileSync(path.resolve(folder, 'index.ts'), "export * from './shared-error-codes'")
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function initTSHelpers() {
|
|
164
|
+
const config = loadConfig()
|
|
165
|
+
const folder = path.resolve(process.cwd(), `${config.coreFolder}${config.paths.infrastructure}`)
|
|
166
|
+
|
|
167
|
+
fs.writeFileSync(path.resolve(folder, 'ts-helpers.ts'), [
|
|
168
|
+
"export type PublicFields<A> = { [k in keyof A]: A[k] }",
|
|
169
|
+
].join('\n'))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function initVO() {
|
|
173
|
+
const config = loadConfig()
|
|
174
|
+
const voFolder = path.resolve(process.cwd(), `${config.coreFolder}${config.paths.valueObjects}`)
|
|
175
|
+
const identifierFolder = path.resolve(voFolder, 'identifier')
|
|
176
|
+
|
|
177
|
+
fs.mkdirSync(identifierFolder, { recursive: true })
|
|
178
|
+
|
|
179
|
+
fs.writeFileSync(path.resolve(identifierFolder, 'identifier.value-object.ts'), [
|
|
180
|
+
"import { randomUUID } from 'node:crypto'",
|
|
181
|
+
"import z from 'zod'",
|
|
182
|
+
"",
|
|
183
|
+
"export type IdentifierModel = z.infer<typeof Identifier.schema>",
|
|
184
|
+
"",
|
|
185
|
+
"export class Identifier {",
|
|
186
|
+
"\tstatic schema = z.string().nonempty()",
|
|
187
|
+
"\t",
|
|
188
|
+
"\tprivate constructor(private readonly data: string) {}",
|
|
189
|
+
"\t",
|
|
190
|
+
"\tstatic create = (data: IdentifierModel) => {",
|
|
191
|
+
"\t\treturn new Identifier(Identifier.schema.parse(data))",
|
|
192
|
+
"\t}",
|
|
193
|
+
"\t",
|
|
194
|
+
"\tstatic get randomUUID() {",
|
|
195
|
+
"\t\treturn Identifier.create(randomUUID())",
|
|
196
|
+
"\t}",
|
|
197
|
+
"\t",
|
|
198
|
+
"\tget model(): IdentifierModel {",
|
|
199
|
+
"\t\treturn this.data",
|
|
200
|
+
"\t}",
|
|
201
|
+
"}"
|
|
202
|
+
].join('\n'))
|
|
203
|
+
|
|
204
|
+
fs.writeFileSync(path.resolve(identifierFolder, 'index.ts'), "export * from './identifier.value-object'")
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function initDI() {
|
|
208
|
+
const config = loadConfig()
|
|
209
|
+
const diPath = config?.paths?.di
|
|
210
|
+
|
|
211
|
+
const folder = path.resolve(process.cwd(), `${config.coreFolder}${diPath}`)
|
|
212
|
+
fs.mkdirSync(folder, { recursive: true })
|
|
213
|
+
|
|
214
|
+
// Entries
|
|
215
|
+
fs.writeFileSync(path.resolve(folder, `entries.di.ts`), [
|
|
216
|
+
"import type { BindInWhenOnFluentSyntax } from 'inversify'",
|
|
217
|
+
"",
|
|
218
|
+
"type DIEntries = Record<",
|
|
219
|
+
"\tstring,",
|
|
220
|
+
"\t| { constantValue: object }",
|
|
221
|
+
"\t| (new (...args: any[]) => any)",
|
|
222
|
+
"\t| Record<'test' | 'dev' | 'prod',",
|
|
223
|
+
"\t\t| [new (...args: any[]) => any, (x: BindInWhenOnFluentSyntax<unknown>) => any]",
|
|
224
|
+
"\t\t| (new (...args: any[]) => any)",
|
|
225
|
+
"\t\t| { constantValue: object }",
|
|
226
|
+
"\t>",
|
|
227
|
+
">",
|
|
228
|
+
"",
|
|
229
|
+
"export const diEntries = {",
|
|
230
|
+
"\t// Stores",
|
|
231
|
+
"\t// Providers",
|
|
232
|
+
"\t// Services",
|
|
233
|
+
"\t// Controllers",
|
|
234
|
+
"\t// Infrastructure",
|
|
235
|
+
"} satisfies DIEntries",
|
|
236
|
+
"",
|
|
237
|
+
"export type DITokens = keyof typeof diEntries",
|
|
238
|
+
].join('\n'))
|
|
239
|
+
|
|
240
|
+
// Containers
|
|
241
|
+
fs.writeFileSync(path.resolve(folder, `container.dev.di.ts`), [
|
|
242
|
+
"import { Container } from 'inversify'",
|
|
243
|
+
"import { diEntries } from './entries.di'",
|
|
244
|
+
"",
|
|
245
|
+
"const container = new Container()",
|
|
246
|
+
"",
|
|
247
|
+
"for (const rule in diEntries) {",
|
|
248
|
+
"\tconst ruleContentRaw = diEntries[rule as keyof typeof diEntries]",
|
|
249
|
+
"\tif ('constantValue' in ruleContentRaw) {",
|
|
250
|
+
"\t\tcontainer.bind(rule).toConstantValue(ruleContentRaw.constantValue)",
|
|
251
|
+
"\t\tcontinue",
|
|
252
|
+
"\t}",
|
|
253
|
+
"\tconst ruleContent =",
|
|
254
|
+
"\t\ttypeof ruleContentRaw === 'object'",
|
|
255
|
+
"\t\t\t? ruleContentRaw.dev",
|
|
256
|
+
"\t\t\t: ruleContentRaw",
|
|
257
|
+
"\tif (Array.isArray(ruleContent)) {",
|
|
258
|
+
"\t\tconst [Entry, builder] = ruleContent",
|
|
259
|
+
"\t\tbuilder(container.bind(rule).to(Entry))",
|
|
260
|
+
"\t\tcontinue",
|
|
261
|
+
"\t}",
|
|
262
|
+
"\tif ('constantValue' in ruleContent) {",
|
|
263
|
+
"\t\tcontainer.bind(rule).toConstantValue(ruleContent.constantValue)",
|
|
264
|
+
"\t\tcontinue",
|
|
265
|
+
"\t}",
|
|
266
|
+
"\tcontainer.bind(rule).to(ruleContent)",
|
|
267
|
+
"}",
|
|
268
|
+
"",
|
|
269
|
+
"export { container as devContainer }"
|
|
270
|
+
].join('\n'))
|
|
271
|
+
|
|
272
|
+
fs.writeFileSync(path.resolve(folder, `container.test.di.ts`), [
|
|
273
|
+
"import { Container } from 'inversify'",
|
|
274
|
+
"import { diEntries } from './entries.di'",
|
|
275
|
+
"",
|
|
276
|
+
"const container = new Container()",
|
|
277
|
+
"",
|
|
278
|
+
"for (const rule in diEntries) {",
|
|
279
|
+
"\tconst ruleContentRaw = diEntries[rule as keyof typeof diEntries]",
|
|
280
|
+
"\tif ('constantValue' in ruleContentRaw) {",
|
|
281
|
+
"\t\tcontainer.bind(rule).toConstantValue(ruleContentRaw.constantValue)",
|
|
282
|
+
"\t\tcontinue",
|
|
283
|
+
"\t}",
|
|
284
|
+
"\tconst ruleContent =",
|
|
285
|
+
"\t\ttypeof ruleContentRaw === 'object'",
|
|
286
|
+
"\t\t\t? ruleContentRaw.test",
|
|
287
|
+
"\t\t\t: ruleContentRaw",
|
|
288
|
+
"\tif (Array.isArray(ruleContent)) {",
|
|
289
|
+
"\t\tconst [Entry, builder] = ruleContent",
|
|
290
|
+
"\t\tbuilder(container.bind(rule).to(Entry))",
|
|
291
|
+
"\t\tcontinue",
|
|
292
|
+
"\t}",
|
|
293
|
+
"\tif ('constantValue' in ruleContent) {",
|
|
294
|
+
"\t\tcontainer.bind(rule).toConstantValue(ruleContent.constantValue)",
|
|
295
|
+
"\t\tcontinue",
|
|
296
|
+
"\t}",
|
|
297
|
+
"\tcontainer.bind(rule).to(ruleContent)",
|
|
298
|
+
"}",
|
|
299
|
+
"",
|
|
300
|
+
"export { container as testContainer }"
|
|
301
|
+
].join('\n'))
|
|
302
|
+
|
|
303
|
+
fs.writeFileSync(path.resolve(folder, `container.prod.di.ts`), [
|
|
304
|
+
"import { Container } from 'inversify'",
|
|
305
|
+
"import { diEntries } from './entries.di'",
|
|
306
|
+
"",
|
|
307
|
+
"const container = new Container()",
|
|
308
|
+
"",
|
|
309
|
+
"for (const rule in diEntries) {",
|
|
310
|
+
"\tconst ruleContentRaw = diEntries[rule as keyof typeof diEntries]",
|
|
311
|
+
"\tif ('constantValue' in ruleContentRaw) {",
|
|
312
|
+
"\t\tcontainer.bind(rule).toConstantValue(ruleContentRaw.constantValue)",
|
|
313
|
+
"\t\tcontinue",
|
|
314
|
+
"\t}",
|
|
315
|
+
"\tconst ruleContent =",
|
|
316
|
+
"\t\ttypeof ruleContentRaw === 'object'",
|
|
317
|
+
"\t\t\t? ruleContentRaw.prod",
|
|
318
|
+
"\t\t\t: ruleContentRaw",
|
|
319
|
+
"\tif (Array.isArray(ruleContent)) {",
|
|
320
|
+
"\t\tconst [Entry, builder] = ruleContent",
|
|
321
|
+
"\t\tbuilder(container.bind(rule).to(Entry))",
|
|
322
|
+
"\t\tcontinue",
|
|
323
|
+
"\t}",
|
|
324
|
+
"\tif ('constantValue' in ruleContent) {",
|
|
325
|
+
"\t\tcontainer.bind(rule).toConstantValue(ruleContent.constantValue)",
|
|
326
|
+
"\t\tcontinue",
|
|
327
|
+
"\t}",
|
|
328
|
+
"\tcontainer.bind(rule).to(ruleContent)",
|
|
329
|
+
"}",
|
|
330
|
+
"",
|
|
331
|
+
"export { container as prodContainer }"
|
|
332
|
+
].join('\n'))
|
|
333
|
+
|
|
334
|
+
// Index
|
|
335
|
+
fs.writeFileSync(path.resolve(folder, `index.ts`), [
|
|
336
|
+
"import 'reflect-metadata'",
|
|
337
|
+
"import { devContainer } from './container.dev.di'",
|
|
338
|
+
"import { prodContainer } from './container.prod.di'",
|
|
339
|
+
"import { testContainer } from './container.test.di'",
|
|
340
|
+
"import type { DITokens } from './entries.di'",
|
|
341
|
+
"",
|
|
342
|
+
"const getActiveContainer = () => {",
|
|
343
|
+
"\tconst environment = process.env.NODE_ENV",
|
|
344
|
+
"\tif (environment === 'test') {",
|
|
345
|
+
"\t\treturn testContainer",
|
|
346
|
+
"\t}",
|
|
347
|
+
"\tif (environment === 'development') {",
|
|
348
|
+
"\t\treturn devContainer",
|
|
349
|
+
"\t}",
|
|
350
|
+
"\treturn prodContainer",
|
|
351
|
+
"}",
|
|
352
|
+
"",
|
|
353
|
+
"export const fromDI = <Result>(key: DITokens) => {",
|
|
354
|
+
"\tconst container = getActiveContainer()",
|
|
355
|
+
"\treturn container.get<Result>(key)",
|
|
356
|
+
"}",
|
|
357
|
+
"",
|
|
358
|
+
"export { DITokens }"
|
|
359
|
+
].join('\n'))
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
function initClientUtils() {
|
|
363
|
+
const config = loadConfig()
|
|
364
|
+
const root = findProjectRoot()
|
|
365
|
+
const folder = path.resolve(root, `${config.coreFolder}${config.paths.clientUtils}`)
|
|
366
|
+
fs.mkdirSync(folder, { recursive: true })
|
|
367
|
+
|
|
368
|
+
fs.writeFileSync(path.resolve(folder, 'api-request.ts'), [
|
|
369
|
+
"export const apiRequest = (url: string, method: 'GET' | 'POST' | 'DELETE' | 'PATCH' | 'PUT') =>",
|
|
370
|
+
"\tasync (payload: { body?: Object; query?: Object }) => {",
|
|
371
|
+
"\t\tconst query = payload?.query ? '?' + new URLSearchParams(Object.entries(payload.query).filter(([, v]) => v != null && v !== '')) : ''",
|
|
372
|
+
"\t\tconst res = await fetch(url + query, { method, headers: { 'Content-Type': 'application/json' }, body: payload?.body && JSON.stringify(payload.body) })",
|
|
373
|
+
"\t\tif (!res.ok) throw await res.json()",
|
|
374
|
+
"\t\treturn res.json()",
|
|
375
|
+
"\t}"
|
|
376
|
+
].join('\n'))
|
|
377
|
+
|
|
378
|
+
fs.writeFileSync(path.resolve(folder, 'normalize-query-key-payload.ts'), [
|
|
379
|
+
'export const normalizeObjectKeysOrder = (input: any): any => {',
|
|
380
|
+
'\tif (Array.isArray(input)) {',
|
|
381
|
+
'\t\treturn input.map(item => normalizeObjectKeysOrder(item))',
|
|
382
|
+
'\t}',
|
|
383
|
+
'',
|
|
384
|
+
'\tif (input !== null && typeof input === "object") {',
|
|
385
|
+
'\t\tconst sortedKeys = Object.keys(input).sort((a, b) => a.localeCompare(b))',
|
|
386
|
+
'\t\tconst result: Record<string, any> = {}',
|
|
387
|
+
'\t\tfor (const key of sortedKeys) {',
|
|
388
|
+
'\t\t\tresult[key] = normalizeObjectKeysOrder(input[key])',
|
|
389
|
+
'\t\t}',
|
|
390
|
+
'\t\treturn result',
|
|
391
|
+
'\t}',
|
|
392
|
+
'',
|
|
393
|
+
'\treturn input',
|
|
394
|
+
'}'
|
|
395
|
+
].join('\n'))
|
|
396
|
+
|
|
397
|
+
fs.writeFileSync(path.resolve(folder, 'index.ts'), [
|
|
398
|
+
`export * from './api-request'`,
|
|
399
|
+
`export * from './normalize-query-key-payload'`
|
|
400
|
+
].join('\n'))
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function initPrisma() {
|
|
404
|
+
const config = loadConfig()
|
|
405
|
+
const prismaClientPath = config?.store?.prisma?.clientPath
|
|
406
|
+
if (!prismaClientPath) {
|
|
407
|
+
return
|
|
408
|
+
}
|
|
409
|
+
const prismaFolder = path.resolve(process.cwd(), `${config.coreFolder}${config?.paths?.infrastructure}`, 'prisma')
|
|
410
|
+
fs.mkdirSync(prismaFolder, { recursive: true })
|
|
411
|
+
|
|
412
|
+
fs.writeFileSync(path.resolve(prismaFolder, 'client.ts'), [
|
|
413
|
+
'/** ! import required Prisma adapter */',
|
|
414
|
+
`import { PrismaClient } from '${prismaClientPath}'`,
|
|
415
|
+
``,
|
|
416
|
+
'const connectionString = `${process.env.DATABASE_URL}`',
|
|
417
|
+
`const adapter = /** ! instanse of the adapter */`,
|
|
418
|
+
``,
|
|
419
|
+
`export const prismaClient = new PrismaClient({ adapter })`
|
|
420
|
+
].join('\n'))
|
|
421
|
+
|
|
422
|
+
fs.writeFileSync(path.resolve(prismaFolder, 'index.ts'), [
|
|
423
|
+
`export * from './client'`
|
|
424
|
+
].join('\n'))
|
|
425
|
+
|
|
426
|
+
// Update DI
|
|
427
|
+
|
|
428
|
+
const diEntriesPath = path.resolve(process.cwd(), `${config.coreFolder}${config?.paths?.di}`, 'entries.di.ts')
|
|
429
|
+
|
|
430
|
+
insertBeforeLineInFile(
|
|
431
|
+
diEntriesPath,
|
|
432
|
+
'type DIEntries =',
|
|
433
|
+
`import { prismaClient } from '@${config?.paths?.infrastructure}/prisma'`
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
insertAfterLineInFile(
|
|
437
|
+
diEntriesPath,
|
|
438
|
+
'// Infrastructure',
|
|
439
|
+
`\tPrismaClient: { constantValue: prismaClient },`,
|
|
440
|
+
)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function generateInfrastructure(upperCase, lowerCase) {
|
|
444
|
+
const config = loadConfig()
|
|
445
|
+
const folder = path.resolve(process.cwd(), `${config.coreFolder}${config?.paths?.infrastructure}`, entityName)
|
|
446
|
+
fs.mkdirSync(folder, { recursive: true })
|
|
447
|
+
|
|
448
|
+
fs.writeFileSync(path.resolve(folder, `${entityName}.ts`), [
|
|
449
|
+
`import { Module } from '@alevnyacow/nzmt'`,
|
|
450
|
+
'',
|
|
451
|
+
`export const ${lowerCase}InfrastructureMetadata = {`,
|
|
452
|
+
`\tname: '${upperCase}Infrastructure',`,
|
|
453
|
+
`\tschemas: {}`,
|
|
454
|
+
`} satisfies Module.Metadata`,
|
|
455
|
+
'',
|
|
456
|
+
`export class ${upperCase} {`,
|
|
457
|
+
`\tprivate methods = Module.methods(${lowerCase}InfrastructureMetadata)`,
|
|
458
|
+
`}`
|
|
459
|
+
].join('\n'))
|
|
460
|
+
|
|
461
|
+
fs.writeFileSync(path.resolve(folder, `${entityName}.mock.ts`), [
|
|
462
|
+
`import { PublicFields } from '@/${config.paths.infrastructure}/ts-helpers'`,
|
|
463
|
+
`import { ${upperCase} } from './${entityName}'`,
|
|
464
|
+
'',
|
|
465
|
+
`export class Mock${upperCase} implements PublicFields<${upperCase}> {`,
|
|
466
|
+
`\t`,
|
|
467
|
+
`}`
|
|
468
|
+
].join('\n'))
|
|
469
|
+
|
|
470
|
+
fs.writeFileSync(path.resolve(folder, `index.ts`), [
|
|
471
|
+
`export * from './${entityName}'`,
|
|
472
|
+
`export * from './${entityName}.mock'`
|
|
473
|
+
].join('\n'))
|
|
474
|
+
|
|
475
|
+
// Update DI
|
|
476
|
+
|
|
477
|
+
const diEntriesPath = path.resolve(process.cwd(), `${config.coreFolder}${config?.paths?.di}`, 'entries.di.ts')
|
|
478
|
+
|
|
479
|
+
insertBeforeLineInFile(
|
|
480
|
+
diEntriesPath,
|
|
481
|
+
'type DIEntries =',
|
|
482
|
+
`import { ${upperCase}, Mock${upperCase} } from '@${config?.paths?.infrastructure}/${entityName}'`
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
insertAfterLineInFile(
|
|
486
|
+
diEntriesPath,
|
|
487
|
+
'// Infrastructure',
|
|
488
|
+
`\t${upperCase}: { test: Mock${upperCase}, dev: ${upperCase}, prod: ${upperCase} },`,
|
|
489
|
+
)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
function initGuards() {
|
|
494
|
+
const config = loadConfig()
|
|
495
|
+
const endpointGuardsFolder = path.resolve(process.cwd(), `${config.coreFolder}${config?.paths?.infrastructure}`, 'guards')
|
|
496
|
+
fs.mkdirSync(endpointGuardsFolder, { recursive: true })
|
|
497
|
+
|
|
498
|
+
fs.writeFileSync(path.resolve(endpointGuardsFolder, 'guards.ts'), [
|
|
499
|
+
`import type { Controller } from '@alevnyacow/nzmt'`,
|
|
500
|
+
'',
|
|
501
|
+
`export class Guards {`,
|
|
502
|
+
`\tdummyGuard: Controller.Guard = async () => { return undefined }`,
|
|
503
|
+
`}`
|
|
504
|
+
].join('\n'))
|
|
505
|
+
|
|
506
|
+
fs.writeFileSync(path.resolve(endpointGuardsFolder, 'index.ts'), [
|
|
507
|
+
`export * from './guards'`
|
|
508
|
+
].join('\n'))
|
|
509
|
+
|
|
510
|
+
// Update DI
|
|
511
|
+
|
|
512
|
+
const diEntriesPath = path.resolve(process.cwd(), `${config.coreFolder}${config?.paths?.di}`, 'entries.di.ts')
|
|
513
|
+
|
|
514
|
+
insertBeforeLineInFile(
|
|
515
|
+
diEntriesPath,
|
|
516
|
+
'type DIEntries =',
|
|
517
|
+
`import { Guards } from '@${config?.paths?.infrastructure}/guards'`
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
insertAfterLineInFile(
|
|
521
|
+
diEntriesPath,
|
|
522
|
+
'// Infrastructure',
|
|
523
|
+
`\tGuards,`,
|
|
524
|
+
)
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function initLogger() {
|
|
528
|
+
const config = loadConfig()
|
|
529
|
+
const loggerFolder = path.resolve(process.cwd(), `${config.coreFolder}${config?.paths?.infrastructure}`, 'logger')
|
|
530
|
+
fs.mkdirSync(loggerFolder, { recursive: true })
|
|
531
|
+
|
|
532
|
+
fs.writeFileSync(path.resolve(loggerFolder, 'logger.ts'), [
|
|
533
|
+
`export abstract class Logger {`,
|
|
534
|
+
`\tabstract error: (payload: Record<string, unknown>) => Promise<void>`,
|
|
535
|
+
`}`
|
|
536
|
+
].join('\n'))
|
|
537
|
+
|
|
538
|
+
fs.writeFileSync(path.resolve(loggerFolder, 'logger.console.ts'), [
|
|
539
|
+
`import { injectable } from 'inversify'`,
|
|
540
|
+
`import { Logger } from './logger'`,
|
|
541
|
+
'',
|
|
542
|
+
'@injectable()',
|
|
543
|
+
`export class ConsoleLogger extends Logger {`,
|
|
544
|
+
`\terror: Logger['error'] = async (payload) => console.error(payload)`,
|
|
545
|
+
`}`
|
|
546
|
+
].join('\n'))
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
fs.writeFileSync(path.resolve(loggerFolder, 'index.ts'), [
|
|
550
|
+
`export * from './logger'`,
|
|
551
|
+
`export * from './logger.console'`
|
|
552
|
+
].join('\n'))
|
|
553
|
+
|
|
554
|
+
// Update DI
|
|
555
|
+
|
|
556
|
+
const diEntriesPath = path.resolve(process.cwd(), `${config.coreFolder}${config?.paths?.di}`, 'entries.di.ts')
|
|
557
|
+
|
|
558
|
+
insertBeforeLineInFile(
|
|
559
|
+
diEntriesPath,
|
|
560
|
+
'type DIEntries =',
|
|
561
|
+
`import { ConsoleLogger } from '@${config?.paths?.infrastructure}/logger'`
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
insertAfterLineInFile(
|
|
565
|
+
diEntriesPath,
|
|
566
|
+
'// Infrastructure',
|
|
567
|
+
`\tLogger: ConsoleLogger,`,
|
|
568
|
+
)
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (command.toLowerCase() === 'init') {
|
|
572
|
+
createDefaultConfig()
|
|
573
|
+
initDI()
|
|
574
|
+
initClientUtils()
|
|
575
|
+
initVO()
|
|
576
|
+
initSharedErrors()
|
|
577
|
+
initPrisma()
|
|
578
|
+
initLogger()
|
|
579
|
+
initGuards()
|
|
580
|
+
initTSHelpers()
|
|
581
|
+
|
|
582
|
+
process.exit(0)
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function generateStores(lowerCase, upperCase, withEntityPreset) {
|
|
586
|
+
const folder = config?.paths?.stores ? path.resolve(process.cwd(), `${config.coreFolder}${config?.paths?.stores}`, entityName) : path.resolve(process.cwd(), entityName);
|
|
587
|
+
|
|
588
|
+
fs.mkdirSync(folder, { recursive: true })
|
|
589
|
+
|
|
590
|
+
const withEntity = withEntityPreset || (options ?? []).includes('import-entity')
|
|
591
|
+
|
|
592
|
+
// Contract
|
|
593
|
+
|
|
594
|
+
fs.writeFileSync(path.resolve(folder, `${entityName}.store.ts`), [
|
|
595
|
+
"import { Store } from '@alevnyacow/nzmt'",
|
|
596
|
+
withEntity ? `import { ${upperCase} } from '@${config?.paths?.entities}/${entityName}'` : undefined,
|
|
597
|
+
"",
|
|
598
|
+
`export const ${lowerCase}StoreMetadata = {`,
|
|
599
|
+
"\tmodels: {",
|
|
600
|
+
withEntity ? `\t\tlist: ${upperCase}.schema,` : "\t\tlist: z.object({ }),",
|
|
601
|
+
withEntity ? `\t\tdetails: ${upperCase}.schema,` : "\t\tdetails: z.object({ }),",
|
|
602
|
+
"\t},",
|
|
603
|
+
"",
|
|
604
|
+
"\tsearchPayload: {",
|
|
605
|
+
withEntity ? `\t\tlist: ${upperCase}.schema.omit({ id: true }).partial(),` : "\t\tlist: z.object({ }),",
|
|
606
|
+
withEntity ? `\t\tspecific: ${upperCase}.schema.pick({ id: true }),` : "\t\tspecific: z.object({ }),",
|
|
607
|
+
"\t},",
|
|
608
|
+
"",
|
|
609
|
+
"\tactionsPayload: {",
|
|
610
|
+
withEntity ? `\t\tcreate: ${upperCase}.schema.omit({ id: true }),` : "\t\tcreate: z.object({ }),",
|
|
611
|
+
withEntity ? `\t\tupdate: ${upperCase}.schema.omit({ id: true }).partial(),` : "\t\tupdate: z.object({ }),",
|
|
612
|
+
"\t},",
|
|
613
|
+
"",
|
|
614
|
+
`\tname: '${upperCase}Store'`,
|
|
615
|
+
"} satisfies Store.Metadata",
|
|
616
|
+
"",
|
|
617
|
+
`export const { schemas: ${lowerCase}StoreSchemas } = Store.toModuleMetadata(${lowerCase}StoreMetadata)`,
|
|
618
|
+
"",
|
|
619
|
+
`export type ${upperCase}Store = Store.Contract<typeof ${lowerCase}StoreMetadata>`
|
|
620
|
+
].filter(x => typeof x === 'string').join('\n'))
|
|
621
|
+
|
|
622
|
+
// RAM
|
|
623
|
+
|
|
624
|
+
fs.writeFileSync(path.resolve(folder, `${entityName}.store.ram.ts`), [
|
|
625
|
+
"import { injectable } from 'inversify'",
|
|
626
|
+
"import { Store } from '@alevnyacow/nzmt'",
|
|
627
|
+
`import { type ${upperCase}Store, ${lowerCase}StoreMetadata } from './${entityName}.store'`,
|
|
628
|
+
"",
|
|
629
|
+
`const CRUDInRAM = Store.InRAM(${lowerCase}StoreMetadata)`,
|
|
630
|
+
"",
|
|
631
|
+
"@injectable()",
|
|
632
|
+
`export class ${upperCase}RAMStore extends CRUDInRAM implements ${upperCase}Store {`,
|
|
633
|
+
"\t",
|
|
634
|
+
"}"
|
|
635
|
+
].filter(x => typeof x === 'string').join('\n'))
|
|
636
|
+
|
|
637
|
+
// Prisma
|
|
638
|
+
const prismaPath = config.store?.prisma?.clientPath
|
|
639
|
+
if (prismaPath) {
|
|
640
|
+
fs.writeFileSync(path.resolve(folder, `${entityName}.store.prisma.ts`), [
|
|
641
|
+
`import type { Prisma, PrismaClient } from '${prismaPath}'`,
|
|
642
|
+
`import { DITokens } from '@${config?.paths?.di}'`,
|
|
643
|
+
"import { injectable, inject } from 'inversify'",
|
|
644
|
+
"import { Store } from '@alevnyacow/nzmt'",
|
|
645
|
+
`import { type ${upperCase}Store, ${lowerCase}StoreMetadata } from './${entityName}.store'`,
|
|
646
|
+
"",
|
|
647
|
+
`type Types = Store.Types<${upperCase}Store>`,
|
|
648
|
+
"",
|
|
649
|
+
"const mappers = {",
|
|
650
|
+
`\ttoFindOnePayload: (source: Types['findOnePayload']): Prisma.${upperCase}WhereUniqueInput => {`,
|
|
651
|
+
"\t\treturn {",
|
|
652
|
+
"\t\t\t",
|
|
653
|
+
"\t\t};",
|
|
654
|
+
"\t},",
|
|
655
|
+
`\ttoFindListPayload: (source: Types['findListPayload']): Prisma.${upperCase}WhereInput => {`,
|
|
656
|
+
"\t\treturn {",
|
|
657
|
+
"\t\t\t",
|
|
658
|
+
"\t\t};",
|
|
659
|
+
"\t},",
|
|
660
|
+
`\ttoListModel: (source: Prisma.${upperCase}GetPayload<{}>): Types['listModel'] => {`,
|
|
661
|
+
"\t\treturn {",
|
|
662
|
+
"\t\t\t",
|
|
663
|
+
"\t\t};",
|
|
664
|
+
"\t},",
|
|
665
|
+
`\ttoDetails: (source: Prisma.${upperCase}GetPayload<{}>): Types['details'] => {`,
|
|
666
|
+
"\t\treturn {",
|
|
667
|
+
"\t\t\t",
|
|
668
|
+
"\t\t};",
|
|
669
|
+
"\t},",
|
|
670
|
+
`\ttoCreatePayload: (source: Types['createPayload']): Prisma.${upperCase}CreateInput => {`,
|
|
671
|
+
"\t\treturn {",
|
|
672
|
+
"\t\t\t",
|
|
673
|
+
"\t\t};",
|
|
674
|
+
"\t},",
|
|
675
|
+
`\ttoUpdatePayload: (source: Types['updatePayload']): Prisma.${upperCase}UpdateInput => {`,
|
|
676
|
+
"\t\treturn {",
|
|
677
|
+
"\t\t\t",
|
|
678
|
+
"\t\t};",
|
|
679
|
+
"\t}",
|
|
680
|
+
"}",
|
|
681
|
+
"",
|
|
682
|
+
"@injectable()",
|
|
683
|
+
`export class ${upperCase}PrismaStore implements ${upperCase}Store {`,
|
|
684
|
+
`\tprivate method = Store.methods(${lowerCase}StoreMetadata);`,
|
|
685
|
+
"",
|
|
686
|
+
"\tconstructor(@inject('PrismaClient' satisfies DITokens) private readonly prismaClient: PrismaClient) {}",
|
|
687
|
+
"",
|
|
688
|
+
"\tlist = this.method('list', async ({ filter, pagination: { pageSize, zeroBasedIndex } = { pageSize: 1000, zeroBasedIndex: 0 }}) => {",
|
|
689
|
+
`\t\tconst list = await this.prismaClient.${lowerCase}.findMany({`,
|
|
690
|
+
"\t\t\twhere: mappers.toFindListPayload(filter),",
|
|
691
|
+
"\t\t\tskip: zeroBasedIndex * pageSize,",
|
|
692
|
+
"\t\t\ttake: pageSize",
|
|
693
|
+
"\t\t})",
|
|
694
|
+
"\t\t",
|
|
695
|
+
"\t\treturn list.map(mappers.toListModel)",
|
|
696
|
+
"\t});",
|
|
697
|
+
"",
|
|
698
|
+
"\tdetails = this.method('details', async ({ filter }) => {",
|
|
699
|
+
`\t\tconst details = await this.prismaClient.${lowerCase}.findUnique({`,
|
|
700
|
+
"\t\t\twhere: mappers.toFindOnePayload(filter)",
|
|
701
|
+
"\t\t})",
|
|
702
|
+
"",
|
|
703
|
+
"\t\tif (!details) {",
|
|
704
|
+
"\t\t\treturn null",
|
|
705
|
+
"\t\t}",
|
|
706
|
+
"",
|
|
707
|
+
"\t\treturn mappers.toDetails(details)",
|
|
708
|
+
"\t});",
|
|
709
|
+
"",
|
|
710
|
+
"\tcreate = this.method('create', async ({ payload }) => {",
|
|
711
|
+
`\t\tconst { id } = await this.prismaClient.${lowerCase}.create({`,
|
|
712
|
+
"\t\t\tdata: mappers.toCreatePayload(payload),",
|
|
713
|
+
"\t\t\tselect: { id: true }",
|
|
714
|
+
"\t\t})",
|
|
715
|
+
"",
|
|
716
|
+
"\t\treturn { id }",
|
|
717
|
+
"\t});",
|
|
718
|
+
"",
|
|
719
|
+
"\tupdateOne = this.method('updateOne', async ({ filter, payload }) => {",
|
|
720
|
+
"\t\ttry {",
|
|
721
|
+
`\t\t\tawait this.prismaClient.${lowerCase}.update({`,
|
|
722
|
+
"\t\t\t\twhere: mappers.toFindOnePayload(filter),",
|
|
723
|
+
"\t\t\t\tdata: mappers.toUpdatePayload(payload),",
|
|
724
|
+
"\t\t\t})",
|
|
725
|
+
"",
|
|
726
|
+
"\t\t\treturn { success: true }",
|
|
727
|
+
"\t\t}",
|
|
728
|
+
"\t\tcatch {",
|
|
729
|
+
"\t\t\treturn { success: false }",
|
|
730
|
+
"\t\t}",
|
|
731
|
+
"\t});",
|
|
732
|
+
"",
|
|
733
|
+
"\tdeleteOne = this.method('deleteOne', async ({ filter }) => {",
|
|
734
|
+
"\t\ttry {",
|
|
735
|
+
`\t\t\tawait this.prismaClient.${lowerCase}.delete({`,
|
|
736
|
+
"\t\t\t\twhere: mappers.toFindOnePayload(filter),",
|
|
737
|
+
"\t\t\t})",
|
|
738
|
+
"",
|
|
739
|
+
"\t\t\treturn { success: true }",
|
|
740
|
+
"\t\t}",
|
|
741
|
+
"\t\tcatch {",
|
|
742
|
+
"\t\t\treturn { success: false }",
|
|
743
|
+
"\t\t}",
|
|
744
|
+
"\t});",
|
|
745
|
+
"};"
|
|
746
|
+
].filter(x => typeof x === 'string').join('\n'))
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
// barrel
|
|
751
|
+
|
|
752
|
+
fs.writeFileSync(path.resolve(folder, 'index.ts'), [
|
|
753
|
+
`export * from './${entityName}.store.ts'`,
|
|
754
|
+
prismaPath ? `export * from './${entityName}.store.prisma.ts'` : undefined,
|
|
755
|
+
`export * from './${entityName}.store.ram.ts'`
|
|
756
|
+
].filter(x => typeof x === 'string').join('\n'))
|
|
757
|
+
|
|
758
|
+
// update DI
|
|
759
|
+
|
|
760
|
+
const diEntriesPath = path.resolve(process.cwd(), `${config.coreFolder}${config?.paths?.di}`, 'entries.di.ts')
|
|
761
|
+
|
|
762
|
+
insertBeforeLineInFile(
|
|
763
|
+
diEntriesPath,
|
|
764
|
+
'type DIEntries =',
|
|
765
|
+
prismaPath ? `import { ${upperCase}PrismaStore, ${upperCase}RAMStore } from '@${config?.paths?.stores}/${entityName}'` : `import { ${upperCase}RAMStore } from '@${config?.paths?.stores}/${entityName}'`
|
|
766
|
+
)
|
|
767
|
+
|
|
768
|
+
insertAfterLineInFile(
|
|
769
|
+
diEntriesPath,
|
|
770
|
+
'// Stores',
|
|
771
|
+
prismaPath ? `\t${upperCase}Store: { test: [${upperCase}RAMStore, (x) => x.inSingletonScope()], prod: ${upperCase}PrismaStore, dev: ${upperCase}PrismaStore },` : `\t${upperCase}Store: { test: [${upperCase}RAMStore, (x) => x.inSingletonScope()], prod: ${upperCase}RAMStore, dev: ${upperCase}RAMStore },`,
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (command.toLowerCase() === 'store' || command === 'cs') {
|
|
777
|
+
var [lowerCase, upperCase] = camelizeVariants(entityName)
|
|
778
|
+
generateStores(lowerCase, upperCase)
|
|
779
|
+
process.exit(0);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function generateEntity(upperCase) {
|
|
783
|
+
const folder = config?.paths?.entities ? path.resolve(process.cwd(), `${config.coreFolder}${config?.paths?.entities}`, entityName) : path.resolve(process.cwd(), entityName);
|
|
784
|
+
const fields = options.filter(x => x.startsWith('f:')).flatMap(x => x.split(':')[1]).join(',').split(',').map(x => x.split('-')).filter(x => x.length === 2)
|
|
785
|
+
|
|
786
|
+
fs.mkdirSync(folder, { recursive: true })
|
|
787
|
+
|
|
788
|
+
const body = [
|
|
789
|
+
"import z from 'zod'",
|
|
790
|
+
`import { Identifier } from '@${config?.paths?.valueObjects}/identifier'`,
|
|
791
|
+
"",
|
|
792
|
+
`export type ${upperCase}Model = z.infer<typeof ${upperCase}.schema>`,
|
|
793
|
+
"",
|
|
794
|
+
`export class ${upperCase} {`,
|
|
795
|
+
"\tstatic schema = z.object({",
|
|
796
|
+
"\t\tid: Identifier.schema,",
|
|
797
|
+
fields.length ?
|
|
798
|
+
fields.map(([fieldName, description]) => {
|
|
799
|
+
return `\t\t${fieldName}: z.${description.split('.').join('().')}(),`
|
|
800
|
+
}).join('\n')
|
|
801
|
+
: "\t\t",
|
|
802
|
+
"\t})",
|
|
803
|
+
"\t",
|
|
804
|
+
`\tprivate constructor(private readonly data: ${upperCase}Model) {}`,
|
|
805
|
+
"\t",
|
|
806
|
+
`\tstatic create = (data: ${upperCase}Model) => {`,
|
|
807
|
+
`\t\tconst parsedModel = ${upperCase}.schema.parse(data)`,
|
|
808
|
+
`\t\treturn new ${upperCase}(parsedModel)`,
|
|
809
|
+
"\t}",
|
|
810
|
+
"\t",
|
|
811
|
+
`\tget model(): ${upperCase}Model {`,
|
|
812
|
+
"\t\treturn this.data",
|
|
813
|
+
"\t}",
|
|
814
|
+
"}"
|
|
815
|
+
].join('\n')
|
|
816
|
+
|
|
817
|
+
fs.writeFileSync(path.resolve(folder, `${entityName}.entity.ts`), body)
|
|
818
|
+
fs.writeFileSync(path.resolve(folder, 'index.ts'), `export * from './${entityName}.entity'`)
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
if (command.toLowerCase() === 'entity' || command === 'e') {
|
|
822
|
+
var [lowerCase, upperCase] = camelizeVariants(entityName)
|
|
823
|
+
generateEntity(upperCase)
|
|
824
|
+
process.exit(0)
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function generateValueObject(upperCase) {
|
|
828
|
+
const folder = config?.paths?.valueObjects ? path.resolve(process.cwd(), `${config.coreFolder}${config?.paths?.valueObjects}`, entityName) : path.resolve(process.cwd(), entityName);
|
|
829
|
+
const fields = options.filter(x => x.startsWith('f:')).flatMap(x => x.split(':')[1]).join(',').split(',').map(x => x.split('-')).filter(x => x.length === 2)
|
|
830
|
+
|
|
831
|
+
fs.mkdirSync(folder, { recursive: true })
|
|
832
|
+
|
|
833
|
+
const body = [
|
|
834
|
+
"import z from 'zod'",
|
|
835
|
+
"",
|
|
836
|
+
`export type ${upperCase}Model = z.infer<typeof ${upperCase}.schema>`,
|
|
837
|
+
"",
|
|
838
|
+
`export class ${upperCase} {`,
|
|
839
|
+
"\tstatic schema = z.object({",
|
|
840
|
+
fields.length ?
|
|
841
|
+
fields.map(([fieldName, description]) => {
|
|
842
|
+
return `\t\t${fieldName}: z.${description.split('.').join('().')}(),`
|
|
843
|
+
}).join('\n')
|
|
844
|
+
: "\t\t",
|
|
845
|
+
"\t})",
|
|
846
|
+
"\t",
|
|
847
|
+
`\tprivate constructor(private readonly data: ${upperCase}Model) {}`,
|
|
848
|
+
"\t",
|
|
849
|
+
`\tstatic create = (data: ${upperCase}Model) => {`,
|
|
850
|
+
`\t\tconst parsedModel = ${upperCase}.schema.parse(data)`,
|
|
851
|
+
`\t\treturn new ${upperCase}(parsedModel)`,
|
|
852
|
+
"\t}",
|
|
853
|
+
"\t",
|
|
854
|
+
`\tget model(): ${upperCase}Model {`,
|
|
855
|
+
"\t\treturn this.data",
|
|
856
|
+
"\t}",
|
|
857
|
+
"}"
|
|
858
|
+
].join('\n')
|
|
859
|
+
|
|
860
|
+
fs.writeFileSync(path.resolve(folder, `${entityName}.value-object.ts`), body)
|
|
861
|
+
fs.writeFileSync(path.resolve(folder, 'index.ts'), `export * from './${entityName}.value-object'`)
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
if (command.toLowerCase() === 'value-object' || command === 'vo') {
|
|
865
|
+
var [lowerCase, upperCase] = camelizeVariants(entityName)
|
|
866
|
+
generateValueObject(upperCase)
|
|
867
|
+
process.exit(0)
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function generateProvider(lowerCase, upperCase) {
|
|
871
|
+
const folder = config?.paths?.providers ? path.resolve(process.cwd(), `${config.coreFolder}${config?.paths?.providers}`, entityName) : path.resolve(process.cwd(), entityName);
|
|
872
|
+
|
|
873
|
+
fs.mkdirSync(folder, { recursive: true })
|
|
874
|
+
|
|
875
|
+
// Provider
|
|
876
|
+
fs.writeFileSync(path.resolve(folder, `${entityName}.provider.ts`), [
|
|
877
|
+
`import { Module } from '@alevnyacow/nzmt'`,
|
|
878
|
+
'',
|
|
879
|
+
`export const ${lowerCase}ProviderMetadata = {`,
|
|
880
|
+
`\tname: '${upperCase}Provider',`,
|
|
881
|
+
`\tschemas: {}`,
|
|
882
|
+
`} satisfies Module.Metadata`,
|
|
883
|
+
'',
|
|
884
|
+
`export class ${upperCase}Provider {`,
|
|
885
|
+
`\tprivate methods = Module.methods(${lowerCase}ProviderMetadata)`,
|
|
886
|
+
`}`
|
|
887
|
+
].join('\n'))
|
|
888
|
+
|
|
889
|
+
// Mock
|
|
890
|
+
fs.writeFileSync(path.resolve(folder, `${entityName}.provider.mock.ts`), [
|
|
891
|
+
`import { PublicFields } from '@/${config.paths.infrastructure}/ts-helpers'`,
|
|
892
|
+
`import { ${upperCase}Provider } from './${entityName}.provider'`,
|
|
893
|
+
'',
|
|
894
|
+
`export class ${upperCase}MockProvider implements PublicFields<${upperCase}Provider> {`,
|
|
895
|
+
`\t`,
|
|
896
|
+
`}`
|
|
897
|
+
].join('\n'))
|
|
898
|
+
|
|
899
|
+
// Barrel
|
|
900
|
+
fs.writeFileSync(path.resolve(folder, `index.ts`), [
|
|
901
|
+
`export * from './${entityName}.provider'`,
|
|
902
|
+
`export * from './${entityName}.provider.mock'`
|
|
903
|
+
].join('\n'))
|
|
904
|
+
|
|
905
|
+
// Update DI
|
|
906
|
+
const diEntriesPath = path.resolve(process.cwd(), `${config.coreFolder}${config?.paths?.di}`, 'entries.di.ts')
|
|
907
|
+
|
|
908
|
+
insertBeforeLineInFile(
|
|
909
|
+
diEntriesPath,
|
|
910
|
+
'type DIEntries =',
|
|
911
|
+
`import { ${upperCase}MockProvider, ${upperCase}Provider } from '@${config?.paths?.providers}/${entityName}'`
|
|
912
|
+
)
|
|
913
|
+
|
|
914
|
+
insertAfterLineInFile(
|
|
915
|
+
diEntriesPath,
|
|
916
|
+
'// Providers',
|
|
917
|
+
`\t${upperCase}Provider: { test: ${upperCase}MockProvider, prod: ${upperCase}Provider, dev: ${upperCase}Provider },`,
|
|
918
|
+
)
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
if (command.toLowerCase() === 'provider' || command === 'p') {
|
|
922
|
+
var [lowerCase, upperCase] = camelizeVariants(entityName)
|
|
923
|
+
generateProvider(lowerCase, upperCase)
|
|
924
|
+
process.exit(0)
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function toKebabFromPascal(str) {
|
|
928
|
+
return str
|
|
929
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
930
|
+
.toLowerCase()
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function generateService(lowerCase, upperCase, store) {
|
|
934
|
+
const folder = config?.paths?.services ? path.resolve(process.cwd(), `${config.coreFolder}${config?.paths?.services}`, entityName) : path.resolve(process.cwd(), entityName);
|
|
935
|
+
const defaultInjections = config?.services?.defaultInjections
|
|
936
|
+
|
|
937
|
+
const proxiedStore = store || options.find(x => x.startsWith('p:'))?.split(':')?.at(-1)
|
|
938
|
+
if (proxiedStore && !proxiedStore.endsWith('Store')) {
|
|
939
|
+
throw 'Only stores can be proxied in services!'
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
let injections = options.filter(x => x.startsWith('i:')).flatMap(x => x.split(':')[1]).join(',').split(',').filter(x => !!x.length)
|
|
943
|
+
if (proxiedStore && !injections.includes(proxiedStore)) {
|
|
944
|
+
injections = injections.concat(proxiedStore)
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
injections = [...defaultInjections, ...injections.filter(x => !defaultInjections.includes(x))]
|
|
948
|
+
|
|
949
|
+
const importInjections = injections.map((i) => {
|
|
950
|
+
if (i.endsWith('Service') || i.endsWith('Controller')) {
|
|
951
|
+
throw 'Only stores and infrastructure can be injected in services'
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
if (i.endsWith('Store')) {
|
|
955
|
+
return `import type { ${i} } from '@${config?.paths?.stores}/${toKebabFromPascal(i).slice(0, -'-store'.length)}'`
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
return `import type { ${i} } from '@${config?.paths?.infrastructure}/${toKebabFromPascal(i)}'`
|
|
959
|
+
})
|
|
960
|
+
|
|
961
|
+
fs.mkdirSync(folder, { recursive: true })
|
|
962
|
+
|
|
963
|
+
// Metadata
|
|
964
|
+
|
|
965
|
+
fs.writeFileSync(path.resolve(folder, `${entityName}.service.metadata.ts`), [
|
|
966
|
+
"import type { Module } from '@alevnyacow/nzmt'",
|
|
967
|
+
proxiedStore ? `import { ${proxiedStore.substring(0, 1).toLowerCase() + proxiedStore.substring(1)}Schemas } from '@${config?.paths?.stores}/${toKebabFromPascal(proxiedStore).slice(0, -'-store'.length)}'` : undefined,
|
|
968
|
+
"",
|
|
969
|
+
`export const ${lowerCase}ServiceMetadata = {`,
|
|
970
|
+
`\tname: '${upperCase}Service',`,
|
|
971
|
+
proxiedStore ? [
|
|
972
|
+
`\tschemas: {`,
|
|
973
|
+
`\t\tgetList: ${proxiedStore.substring(0, 1).toLowerCase() + proxiedStore.substring(1)}Schemas.list,`,
|
|
974
|
+
`\t\tgetDetails: ${proxiedStore.substring(0, 1).toLowerCase() + proxiedStore.substring(1)}Schemas.details,`,
|
|
975
|
+
`\t\tcreate: ${proxiedStore.substring(0, 1).toLowerCase() + proxiedStore.substring(1)}Schemas.create,`,
|
|
976
|
+
`\t\tupdate: ${proxiedStore.substring(0, 1).toLowerCase() + proxiedStore.substring(1)}Schemas.updateOne,`,
|
|
977
|
+
`\t\tdelete: ${proxiedStore.substring(0, 1).toLowerCase() + proxiedStore.substring(1)}Schemas.deleteOne,`,
|
|
978
|
+
`\t}`,
|
|
979
|
+
].join('\n') : "\tschemas: {}",
|
|
980
|
+
"} satisfies Module.Metadata",
|
|
981
|
+
"",
|
|
982
|
+
`export type ${upperCase}ServiceDTOs = Module.DTOs<typeof ${lowerCase}ServiceMetadata>`
|
|
983
|
+
].filter(x => typeof x === 'string').join('\n'))
|
|
984
|
+
|
|
985
|
+
|
|
986
|
+
// Service body
|
|
987
|
+
|
|
988
|
+
fs.writeFileSync(path.resolve(folder, `${entityName}.service.ts`), [
|
|
989
|
+
"import { injectable, inject } from 'inversify'",
|
|
990
|
+
injections.length ? `import { DITokens } from '@${config?.paths?.di}'` : undefined,
|
|
991
|
+
`import { ${lowerCase}ServiceMetadata } from './${entityName}.service.metadata'`,
|
|
992
|
+
"import { Module } from '@alevnyacow/nzmt'",
|
|
993
|
+
...importInjections,
|
|
994
|
+
"",
|
|
995
|
+
`type Methods = Module.Methods<typeof ${lowerCase}ServiceMetadata>`,
|
|
996
|
+
"",
|
|
997
|
+
"@injectable()",
|
|
998
|
+
`export class ${upperCase}Service implements Methods {`,
|
|
999
|
+
`\tprivate methods = Module.methods(${lowerCase}ServiceMetadata)`,
|
|
1000
|
+
``,
|
|
1001
|
+
`\tconstructor(`,
|
|
1002
|
+
...injections.map(x => `\t\t@inject('${x}' satisfies DITokens) private readonly ${x.charAt(0).toLowerCase() + x.slice(1)}: ${x},`),
|
|
1003
|
+
`\t) {}`,
|
|
1004
|
+
``,
|
|
1005
|
+
proxiedStore ? `\tgetList = this.methods('getList', this.${proxiedStore.charAt(0).toLowerCase() + proxiedStore.slice(1)}.list)` : undefined,
|
|
1006
|
+
proxiedStore ? `\tgetDetails = this.methods('getDetails', this.${proxiedStore.charAt(0).toLowerCase() + proxiedStore.slice(1)}.details)` : undefined,
|
|
1007
|
+
proxiedStore ? `\tcreate = this.methods('create', this.${proxiedStore.charAt(0).toLowerCase() + proxiedStore.slice(1)}.create)` : undefined,
|
|
1008
|
+
proxiedStore ? `\tupdate = this.methods('update', this.${proxiedStore.charAt(0).toLowerCase() + proxiedStore.slice(1)}.updateOne)` : undefined,
|
|
1009
|
+
proxiedStore ? `\tdelete = this.methods('delete', this.${proxiedStore.charAt(0).toLowerCase() + proxiedStore.slice(1)}.deleteOne)` : undefined,
|
|
1010
|
+
"}"
|
|
1011
|
+
].filter(x => typeof x === 'string').join('\n'))
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
// Barrel
|
|
1015
|
+
|
|
1016
|
+
fs.writeFileSync(path.resolve(folder, 'index.ts'), [
|
|
1017
|
+
`export * from './${entityName}.service.metadata'`,
|
|
1018
|
+
`export * from './${entityName}.service'`
|
|
1019
|
+
].join('\n'))
|
|
1020
|
+
|
|
1021
|
+
// Update DI
|
|
1022
|
+
|
|
1023
|
+
const diEntriesPath = path.resolve(process.cwd(), `${config.coreFolder}${config?.paths?.di}`, 'entries.di.ts')
|
|
1024
|
+
|
|
1025
|
+
insertBeforeLineInFile(
|
|
1026
|
+
diEntriesPath,
|
|
1027
|
+
'type DIEntries =',
|
|
1028
|
+
`import { ${upperCase}Service } from '@${config?.paths?.services}/${entityName}'`
|
|
1029
|
+
)
|
|
1030
|
+
|
|
1031
|
+
insertAfterLineInFile(
|
|
1032
|
+
diEntriesPath,
|
|
1033
|
+
'// Services',
|
|
1034
|
+
`\t${upperCase}Service,`,
|
|
1035
|
+
)
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
if (command.toLowerCase() === 'service' || command === 's') {
|
|
1039
|
+
var [lowerCase, upperCase] = camelizeVariants(entityName)
|
|
1040
|
+
generateService(lowerCase, upperCase, false)
|
|
1041
|
+
process.exit(0)
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
function generateController(upperCase, lowerCase, crudService) {
|
|
1045
|
+
if (crudService && !crudService.endsWith('Service')) {
|
|
1046
|
+
throw 'Incorrect crudService'
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
const folder = config?.paths?.controllers ? path.resolve(process.cwd(), `${config.coreFolder}${config?.paths?.controllers}`, entityName) : path.resolve(process.cwd(), entityName);
|
|
1050
|
+
|
|
1051
|
+
let injections = options.filter(x => x.startsWith('i:')).flatMap(x => x.split(':')[1]).join(',').split(',').filter(x => !!x.length)
|
|
1052
|
+
if (crudService && !injections.includes(crudService)) {
|
|
1053
|
+
injections = injections.concat(crudService)
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
injections = ['Logger', 'Guards', ...injections.filter(x => x !== 'Logger' && x !== 'Guards')]
|
|
1057
|
+
|
|
1058
|
+
const crudServiceLowercase = crudService ? crudService.substring(0, 1).toLowerCase() + crudService.substring(1) : undefined
|
|
1059
|
+
|
|
1060
|
+
const importInjections = injections.map((i) => {
|
|
1061
|
+
if (i.endsWith('Controller')) {
|
|
1062
|
+
throw 'Only stores, services and infrastructure can be injected in controllers'
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
if (i.endsWith('Store')) {
|
|
1066
|
+
return `import type { ${i} } from '@${config?.paths?.stores}/${toKebabFromPascal(i).slice(0, -'-store'.length)}'`
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
if (i.endsWith('Service')) {
|
|
1070
|
+
return `import type { ${i} } from '@${config?.paths?.services}/${toKebabFromPascal(i).slice(0, -'-service'.length)}'`
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
return `import type { ${i} } from '@${config?.paths?.infrastructure}/${toKebabFromPascal(i)}'`
|
|
1074
|
+
})
|
|
1075
|
+
|
|
1076
|
+
fs.mkdirSync(folder, { recursive: true })
|
|
1077
|
+
|
|
1078
|
+
// Metadata
|
|
1079
|
+
|
|
1080
|
+
fs.writeFileSync(path.resolve(folder, `${entityName}.controller.metadata.ts`), [
|
|
1081
|
+
crudService ? `import z from 'zod'` : undefined,
|
|
1082
|
+
`import { Controller, ValueObjects } from '@alevnyacow/nzmt'`,
|
|
1083
|
+
crudService ? `import { ${crudServiceLowercase}Metadata } from '@${config.paths.services}/${toKebabFromPascal(crudService).slice(0, -'-service'.length)}'` : undefined,
|
|
1084
|
+
``,
|
|
1085
|
+
`export const ${lowerCase}ControllerMetadata = {`,
|
|
1086
|
+
`\tname: '${upperCase}Controller',`,
|
|
1087
|
+
crudService ? [
|
|
1088
|
+
`\tschemas: {`,
|
|
1089
|
+
`\t\tGET: {`,
|
|
1090
|
+
`\t\t\tquery: z.union([`,
|
|
1091
|
+
`\t\t\t\t// Request all items`,
|
|
1092
|
+
`\t\t\t\t${crudServiceLowercase}Metadata.schemas.getList.payload.shape.filter,`,
|
|
1093
|
+
'\t\t\t\t// Request specific page',
|
|
1094
|
+
`\t\t\t\tValueObjects.Pagination.schema.extend(${crudServiceLowercase}Metadata.schemas.getList.payload.shape.filter.shape),`,
|
|
1095
|
+
`\t\t\t]),`,
|
|
1096
|
+
`\t\t\tresponse: ${crudServiceLowercase}Metadata.schemas.getList.response`,
|
|
1097
|
+
`\t\t},`,
|
|
1098
|
+
`\t\tdetails_GET: {`,
|
|
1099
|
+
`\t\t\tquery: ${crudServiceLowercase}Metadata.schemas.getDetails.payload,`,
|
|
1100
|
+
'\t\t\t// unwrap() to remove null',
|
|
1101
|
+
`\t\t\tresponse: ${crudServiceLowercase}Metadata.schemas.getDetails.response.unwrap()`,
|
|
1102
|
+
`\t\t},`,
|
|
1103
|
+
`\t\tPOST: {`,
|
|
1104
|
+
`\t\t\tbody: ${crudServiceLowercase}Metadata.schemas.create.payload,`,
|
|
1105
|
+
`\t\t\tresponse: ${crudServiceLowercase}Metadata.schemas.create.response`,
|
|
1106
|
+
`\t\t},`,
|
|
1107
|
+
`\t\tPATCH: {`,
|
|
1108
|
+
`\t\t\tbody: ${crudServiceLowercase}Metadata.schemas.update.payload,`,
|
|
1109
|
+
`\t\t\tresponse: ${crudServiceLowercase}Metadata.schemas.update.response`,
|
|
1110
|
+
`\t\t},`,
|
|
1111
|
+
`\t\tDELETE: {`,
|
|
1112
|
+
`\t\t\tquery: ${crudServiceLowercase}Metadata.schemas.delete.payload.shape.filter,`,
|
|
1113
|
+
`\t\t\tresponse: ${crudServiceLowercase}Metadata.schemas.delete.response`,
|
|
1114
|
+
`\t\t},`,
|
|
1115
|
+
`\t}`,
|
|
1116
|
+
].join('\n') : `\tschemas: {}`,
|
|
1117
|
+
`} satisfies Controller.Metadata`,
|
|
1118
|
+
``,
|
|
1119
|
+
`export type ${upperCase}API = Controller.Contract<typeof ${lowerCase}ControllerMetadata>`
|
|
1120
|
+
].filter(x => typeof x === 'string').join('\n'))
|
|
1121
|
+
|
|
1122
|
+
// Body
|
|
1123
|
+
|
|
1124
|
+
fs.writeFileSync(path.resolve(folder, `${entityName}.controller.ts`), [
|
|
1125
|
+
`import { injectable, inject } from 'inversify'`,
|
|
1126
|
+
`import { Controller } from '@alevnyacow/nzmt'`,
|
|
1127
|
+
`import { DITokens } from '@${config?.paths?.di}'`,
|
|
1128
|
+
crudService ? `import { CommonErrorCodes } from '@${config?.paths?.sharedErrors}'` : undefined,
|
|
1129
|
+
`import { ${lowerCase}ControllerMetadata } from './${entityName}.controller.metadata'`,
|
|
1130
|
+
...importInjections,
|
|
1131
|
+
``,
|
|
1132
|
+
`type Endpoints = Controller.EndpointList<typeof ${lowerCase}ControllerMetadata>`,
|
|
1133
|
+
``,
|
|
1134
|
+
`@injectable()`,
|
|
1135
|
+
`export class ${upperCase}Controller implements Endpoints {`,
|
|
1136
|
+
`\tconstructor(`,
|
|
1137
|
+
...injections.map(x => `\t\t@inject('${x}' satisfies DITokens) private readonly ${x.charAt(0).toLowerCase() + x.slice(1)}: ${x},`),
|
|
1138
|
+
`\t) {}`,
|
|
1139
|
+
``,
|
|
1140
|
+
`\tprivate readonly endpoints = Controller.endpoints(`,
|
|
1141
|
+
`\t\t${lowerCase}ControllerMetadata,`,
|
|
1142
|
+
'\t\t{ onErrorHandlers: [this.logger.error], guards: [] }',
|
|
1143
|
+
'\t)',
|
|
1144
|
+
``,
|
|
1145
|
+
crudService ? [
|
|
1146
|
+
`\tGET = this.endpoints('GET', async (x) => {`,
|
|
1147
|
+
`\t\tif ('pageSize' in x && 'zeroBasedIndex' in x) {`,
|
|
1148
|
+
`\t\t\tconst { pageSize, zeroBasedIndex, ...filter } = x`,
|
|
1149
|
+
`\t\t\treturn await this.${crudServiceLowercase}.getList({ filter, pagination: { pageSize, zeroBasedIndex } })`,
|
|
1150
|
+
`\t\t}`,
|
|
1151
|
+
`\t\treturn await this.${crudServiceLowercase}.getList({ filter: x })`,
|
|
1152
|
+
`\t})`,
|
|
1153
|
+
``,
|
|
1154
|
+
`\tdetails_GET = this.endpoints('details_GET', async (payload, { endpointError }) => {`,
|
|
1155
|
+
`\t\tconst details = await this.${crudServiceLowercase}.getDetails(payload)`,
|
|
1156
|
+
`\t\tif (!details) { throw endpointError(CommonErrorCodes.NO_DATA_WAS_FOUND, 404) }`,
|
|
1157
|
+
`\t\treturn details`,
|
|
1158
|
+
`\t})`,
|
|
1159
|
+
``,
|
|
1160
|
+
`\tPOST = this.endpoints('POST', this.${crudServiceLowercase}.create)`,
|
|
1161
|
+
`\tPATCH = this.endpoints('PATCH', this.${crudServiceLowercase}.update)`,
|
|
1162
|
+
`\tDELETE = this.endpoints('DELETE', (filter) => this.${crudServiceLowercase}.delete({ filter }))`,
|
|
1163
|
+
].join('\n') : undefined,
|
|
1164
|
+
`}`
|
|
1165
|
+
].filter(x => typeof x === 'string').join('\n'))
|
|
1166
|
+
|
|
1167
|
+
// Barrel
|
|
1168
|
+
|
|
1169
|
+
fs.writeFileSync(path.resolve(folder, `index.ts`), [
|
|
1170
|
+
`export * from './${entityName}.controller'`,
|
|
1171
|
+
`export * from './${entityName}.controller.metadata'`
|
|
1172
|
+
].filter(x => typeof x === 'string').join('\n'))
|
|
1173
|
+
|
|
1174
|
+
// Update DI
|
|
1175
|
+
|
|
1176
|
+
const diEntriesPath = path.resolve(process.cwd(), `${config.coreFolder}${config?.paths?.di}`, 'entries.di.ts')
|
|
1177
|
+
|
|
1178
|
+
insertBeforeLineInFile(
|
|
1179
|
+
diEntriesPath,
|
|
1180
|
+
'type DIEntries =',
|
|
1181
|
+
`import { ${upperCase}Controller } from '@${config?.paths?.controllers}/${entityName}'`
|
|
1182
|
+
)
|
|
1183
|
+
|
|
1184
|
+
insertAfterLineInFile(
|
|
1185
|
+
diEntriesPath,
|
|
1186
|
+
'// Controllers',
|
|
1187
|
+
`\t${upperCase}Controller,`,
|
|
1188
|
+
)
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
function generateAPIRoutes(lowerCase, upperCase, entity) {
|
|
1192
|
+
const requiredEntity = entity || entityName
|
|
1193
|
+
const projectRoot = findProjectRoot()
|
|
1194
|
+
const fileText = fs.readFileSync(
|
|
1195
|
+
path.resolve(projectRoot, `${config.coreFolder}${config.paths.controllers}`, requiredEntity, `${requiredEntity}.controller.ts`),
|
|
1196
|
+
'utf-8'
|
|
1197
|
+
)
|
|
1198
|
+
|
|
1199
|
+
const regex = /^\s*(\w+)\s*=\s*this\.endpoints/mg
|
|
1200
|
+
const methods = Array.from(fileText.matchAll(regex), m => m[1])
|
|
1201
|
+
|
|
1202
|
+
const methodInfo = methods.map(method => ({method: method.split('_').pop(), path: method.split('_').slice(0, -1).join('/')}))
|
|
1203
|
+
|
|
1204
|
+
const rootMethods = methodInfo.filter(x => !x.path.length).map(x => x.method)
|
|
1205
|
+
const nestedMethods = methodInfo.filter(x => !!x.path.length).reduce((acc, cur) => {
|
|
1206
|
+
if (!acc[cur.path]) {
|
|
1207
|
+
acc[cur.path] = []
|
|
1208
|
+
}
|
|
1209
|
+
acc[cur.path].push(cur.method)
|
|
1210
|
+
return acc
|
|
1211
|
+
}, {})
|
|
1212
|
+
|
|
1213
|
+
const controllerHandlersRootPath = path.resolve(projectRoot, config.coreFolder, 'app', 'api', `${requiredEntity}-controller`)
|
|
1214
|
+
|
|
1215
|
+
if (fs.existsSync(controllerHandlersRootPath)) {
|
|
1216
|
+
fs.rmSync(controllerHandlersRootPath, { force: true, recursive: true })
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
fs.mkdirSync(controllerHandlersRootPath, { recursive: true })
|
|
1220
|
+
|
|
1221
|
+
if (rootMethods.length) {
|
|
1222
|
+
fs.writeFileSync(path.resolve(controllerHandlersRootPath, 'route.ts'), [
|
|
1223
|
+
`import type { ${upperCase}Controller } from '@${config.paths.controllers}/${requiredEntity}'`,
|
|
1224
|
+
`import { fromDI } from '@${config.paths.di}'`,
|
|
1225
|
+
'',
|
|
1226
|
+
`const controller = fromDI<${upperCase}Controller>('${upperCase}Controller')`,
|
|
1227
|
+
'',
|
|
1228
|
+
rootMethods.map(x => `export const ${x} = controller.${x}`).join('\n')
|
|
1229
|
+
].join('\n'))
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
for (const [currentPath, methods] of Object.entries(nestedMethods)) {
|
|
1233
|
+
const nestedFolder = path.resolve(controllerHandlersRootPath, currentPath)
|
|
1234
|
+
fs.mkdirSync(nestedFolder, { recursive: true })
|
|
1235
|
+
fs.writeFileSync(path.resolve(nestedFolder, 'route.ts'), [
|
|
1236
|
+
`import type { ${upperCase}Controller } from '@${config.paths.controllers}/${requiredEntity}'`,
|
|
1237
|
+
`import { fromDI } from '@${config.paths.di}'`,
|
|
1238
|
+
'',
|
|
1239
|
+
`const controller = fromDI<${upperCase}Controller>('${upperCase}Controller')`,
|
|
1240
|
+
'',
|
|
1241
|
+
methods.map(x => `export const ${x} = controller.${currentPath.replaceAll('/', '_')}_${x}`).join('\n')
|
|
1242
|
+
].join('\n'))
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
function generateQueries(lowerCase, upperCase, entity) {
|
|
1247
|
+
const requiredEntity = entity || entityName
|
|
1248
|
+
const projectRoot = findProjectRoot()
|
|
1249
|
+
|
|
1250
|
+
const fileText = fs.readFileSync(
|
|
1251
|
+
path.resolve(projectRoot, `${config.coreFolder}${config.paths.controllers}`, requiredEntity, `${requiredEntity}.controller.ts`),
|
|
1252
|
+
'utf-8'
|
|
1253
|
+
)
|
|
1254
|
+
|
|
1255
|
+
const regex = /^\s*(\w+)\s*=\s*this\.endpoints/mg
|
|
1256
|
+
const methods = Array.from(fileText.matchAll(regex), m => m[1])
|
|
1257
|
+
|
|
1258
|
+
const methodInfo = methods.map(method => ({method: method.split('_').pop(), path: method.split('_').slice(0, -1).join('/')}))
|
|
1259
|
+
|
|
1260
|
+
const rootMethods = methodInfo.filter(x => !x.path.length).map(x => x.method)
|
|
1261
|
+
const nestedMethods = methodInfo.filter(x => !!x.path.length).reduce((acc, cur) => {
|
|
1262
|
+
if (!acc[cur.path]) {
|
|
1263
|
+
acc[cur.path] = []
|
|
1264
|
+
}
|
|
1265
|
+
acc[cur.path].push(cur.method)
|
|
1266
|
+
return acc
|
|
1267
|
+
}, {})
|
|
1268
|
+
|
|
1269
|
+
const controllerQueriesPath = path.resolve(projectRoot, `${config.coreFolder}${config.paths.queries}`, `${requiredEntity}`, 'endpoints')
|
|
1270
|
+
let scaffoldedMethods = []
|
|
1271
|
+
|
|
1272
|
+
fs.mkdirSync(controllerQueriesPath, { recursive: true })
|
|
1273
|
+
|
|
1274
|
+
for (const rootMethod of rootMethods) {
|
|
1275
|
+
scaffoldedMethods.push(rootMethod)
|
|
1276
|
+
if (!rootMethod) {
|
|
1277
|
+
continue
|
|
1278
|
+
}
|
|
1279
|
+
const fileName = path.resolve(controllerQueriesPath, `${rootMethod}.ts` )
|
|
1280
|
+
const alreadyExists = fs.existsSync(fileName)
|
|
1281
|
+
if (alreadyExists) {
|
|
1282
|
+
continue
|
|
1283
|
+
}
|
|
1284
|
+
fs.writeFileSync(fileName, [
|
|
1285
|
+
`import { ${rootMethod === 'GET' ? 'useQuery' : 'useMutation, useQueryClient'} } from '@tanstack/react-query'`,
|
|
1286
|
+
`import type { ${upperCase}API } from '@${config.paths.controllers}/${requiredEntity}'`,
|
|
1287
|
+
`import { apiRequest${rootMethod === 'GET' ? ', normalizeObjectKeysOrder' : ''} } from '@${config.paths.clientUtils}'`,
|
|
1288
|
+
'',
|
|
1289
|
+
`type Method = ${upperCase}API['endpoints']['${rootMethod}']`,
|
|
1290
|
+
``,
|
|
1291
|
+
`const endpoint = '/api/${requiredEntity}-controller'`,
|
|
1292
|
+
``,
|
|
1293
|
+
rootMethod === 'GET'
|
|
1294
|
+
? [
|
|
1295
|
+
`export const use${rootMethod} = (payload: Method['payload']) => {`,
|
|
1296
|
+
`\treturn useQuery<Method['response'], Method['error']>({`,
|
|
1297
|
+
`\t\tqueryKey: ['${requiredEntity}', '${rootMethod}', normalizeObjectKeysOrder(payload)],`,
|
|
1298
|
+
`\t\tqueryFn: () => apiRequest(endpoint, 'GET')(payload)`,
|
|
1299
|
+
`\t})`,
|
|
1300
|
+
`}`
|
|
1301
|
+
].join('\n')
|
|
1302
|
+
: [
|
|
1303
|
+
`export const use${rootMethod} = () => {`,
|
|
1304
|
+
`\tconst queryClient = useQueryClient()`,
|
|
1305
|
+
`\treturn useMutation<Method['response'], Method['error'], Method['payload']>({`,
|
|
1306
|
+
`\t\tmutationFn: apiRequest(endpoint, '${rootMethod}'),`,
|
|
1307
|
+
`\t\tonSuccess: () => { queryClient.invalidateQueries({ queryKey: ['${requiredEntity}'], exact: false }) }`,
|
|
1308
|
+
`\t})`,
|
|
1309
|
+
`}`
|
|
1310
|
+
].join('\n')
|
|
1311
|
+
].join('\n'))
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
for (const [currentPath, methods] of Object.entries(nestedMethods)) {
|
|
1315
|
+
for (const method of methods) {
|
|
1316
|
+
const fullMethodName = `${currentPath.replaceAll('/', '_')}_${method}`
|
|
1317
|
+
scaffoldedMethods.push(fullMethodName)
|
|
1318
|
+
const fileName = path.resolve(controllerQueriesPath, `${fullMethodName}.ts`)
|
|
1319
|
+
const alreadyExists = fs.existsSync(fileName)
|
|
1320
|
+
if (alreadyExists) {
|
|
1321
|
+
continue
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
const nameForHook = (fullMethodName.charAt(0).toUpperCase() + fullMethodName.slice(1)).replaceAll('_', '');
|
|
1325
|
+
|
|
1326
|
+
fs.writeFileSync(fileName, [
|
|
1327
|
+
`import { ${method === 'GET' ? 'useQuery' : 'useMutation, useQueryClient' } } from '@tanstack/react-query'`,
|
|
1328
|
+
`import type { ${upperCase}API } from '@${config.paths.controllers}/${requiredEntity}'`,
|
|
1329
|
+
`import { apiRequest${method === 'GET' ? ', normalizeObjectKeysOrder' : ''} } from '@${config.paths.clientUtils}'`,
|
|
1330
|
+
'',
|
|
1331
|
+
`type Method = ${upperCase}API['endpoints']['${fullMethodName}']`,
|
|
1332
|
+
``,
|
|
1333
|
+
`const endpoint = '/api/${requiredEntity}-controller/${currentPath}'`,
|
|
1334
|
+
``,
|
|
1335
|
+
method === 'GET'
|
|
1336
|
+
? [
|
|
1337
|
+
`export const use${nameForHook} = (payload: Method['payload']) => {`,
|
|
1338
|
+
`\treturn useQuery<Method['response'], Method['error']>({`,
|
|
1339
|
+
`\t\tqueryKey: ['${requiredEntity}', ${currentPath.split('/').map(x => `'${x}'`).join(', ')}, normalizeObjectKeysOrder(payload)],`,
|
|
1340
|
+
`\t\tqueryFn: () => apiRequest(endpoint, 'GET')(payload)`,
|
|
1341
|
+
`\t})`,
|
|
1342
|
+
`}`
|
|
1343
|
+
].join('\n')
|
|
1344
|
+
: [
|
|
1345
|
+
`export const use${nameForHook} = () => {`,
|
|
1346
|
+
`\tconst queryClient = useQueryClient()`,
|
|
1347
|
+
`\treturn useMutation<Method['response'], Method['error'], Method['payload']>({`,
|
|
1348
|
+
`\t\tmutationFn: apiRequest(endpoint, '${method}'),`,
|
|
1349
|
+
`\t\tonSuccess: () => { queryClient.invalidateQueries({ queryKey: ['${requiredEntity}'], exact: false }) }`,
|
|
1350
|
+
`\t})`,
|
|
1351
|
+
`}`
|
|
1352
|
+
].join('\n')
|
|
1353
|
+
].join('\n'))
|
|
1354
|
+
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
const allQueryFiles = fs.readdirSync(controllerQueriesPath, { withFileTypes: true }).filter(x => x.isFile())
|
|
1359
|
+
const deprecatedQueries = allQueryFiles.filter(x => scaffoldedMethods.every(scaffolded => !x.name.startsWith(scaffolded))).map(x => x.name)
|
|
1360
|
+
|
|
1361
|
+
for (const deprecated of deprecatedQueries) {
|
|
1362
|
+
fs.rmSync(path.resolve(controllerQueriesPath, deprecated))
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
fs.writeFileSync(path.resolve(controllerQueriesPath, 'index.ts'), scaffoldedMethods.map(x => `export * from './${x}'`).join('\n'))
|
|
1366
|
+
|
|
1367
|
+
const indexPath = path.resolve(projectRoot, `${config.coreFolder}${config.paths.queries}`, `${requiredEntity}`)
|
|
1368
|
+
fs.writeFileSync(path.resolve(indexPath, 'index.ts'), `export * as ${upperCase}Queries from './endpoints'`)
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
|
|
1372
|
+
|
|
1373
|
+
if (command === 'api-routes') {
|
|
1374
|
+
var [lowerCase, upperCase] = camelizeVariants(entityName)
|
|
1375
|
+
|
|
1376
|
+
generateAPIRoutes(lowerCase, upperCase)
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
if (command === 'queries') {
|
|
1380
|
+
var [lowerCase, upperCase] = camelizeVariants(entityName)
|
|
1381
|
+
|
|
1382
|
+
generateQueries(lowerCase, upperCase)
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
if (command.toLowerCase() === 'routes-with-queries' || command === 'rq') {
|
|
1386
|
+
const projectRoot = findProjectRoot()
|
|
1387
|
+
const controllersFolder = path.resolve(projectRoot, `${config.coreFolder}${config.paths.controllers}`)
|
|
1388
|
+
const controllerEntities = fs.readdirSync(controllersFolder, { withFileTypes: true }).filter(x => x.isDirectory()).map(x => x.name)
|
|
1389
|
+
for (const entity of controllerEntities) {
|
|
1390
|
+
var [lowerCase, upperCase] = camelizeVariants(entity)
|
|
1391
|
+
generateAPIRoutes(lowerCase, upperCase, entity)
|
|
1392
|
+
generateQueries(lowerCase, upperCase, entity)
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
if (command.toLowerCase() === 'controller' || command === 'c') {
|
|
1397
|
+
var [lowerCase, upperCase] = camelizeVariants(entityName)
|
|
1398
|
+
generateController(upperCase, lowerCase)
|
|
1399
|
+
process.exit(0)
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
if (command.toLowerCase() === 'infrastructure' || command === 'i') {
|
|
1403
|
+
var [lowerCase, upperCase] = camelizeVariants(entityName)
|
|
1404
|
+
generateInfrastructure(upperCase, lowerCase)
|
|
1405
|
+
process.exit(0)
|
|
1406
|
+
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
if (command.toLowerCase() === 'stored-entity' || command === 'se') {
|
|
1410
|
+
var [lowerCase, upperCase] = camelizeVariants(entityName)
|
|
1411
|
+
generateEntity(upperCase)
|
|
1412
|
+
generateStores(lowerCase, upperCase, true)
|
|
1413
|
+
process.exit(0)
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
if (command.toLowerCase() === 'crud-service') {
|
|
1417
|
+
var [lowerCase, upperCase] = camelizeVariants(entityName)
|
|
1418
|
+
generateEntity(upperCase)
|
|
1419
|
+
generateStores(lowerCase, upperCase, true)
|
|
1420
|
+
generateService(lowerCase, upperCase, upperCase + 'Store')
|
|
1421
|
+
process.exit(0)
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
if (command.toLowerCase() === 'crud-api') {
|
|
1425
|
+
var [lowerCase, upperCase] = camelizeVariants(entityName)
|
|
1426
|
+
generateEntity(upperCase)
|
|
1427
|
+
generateStores(lowerCase, upperCase, true)
|
|
1428
|
+
generateService(lowerCase, upperCase, upperCase + 'Store')
|
|
1429
|
+
generateController(upperCase, lowerCase, upperCase + 'Service')
|
|
1430
|
+
generateAPIRoutes(lowerCase, upperCase)
|
|
1431
|
+
generateQueries(lowerCase, upperCase)
|
|
1432
|
+
process.exit(0)
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
function generateWidget(name, rootPath) {
|
|
1436
|
+
const [lowerCase, upperCase] = camelizeVariants(name)
|
|
1437
|
+
|
|
1438
|
+
const widgetsPath = config.paths.widgets
|
|
1439
|
+
const root = findProjectRoot()
|
|
1440
|
+
|
|
1441
|
+
const folder = path.resolve(root, `${config.coreFolder}${widgetsPath}`, rootPath ?? '.', name)
|
|
1442
|
+
fs.mkdirSync(folder, { recursive: true })
|
|
1443
|
+
|
|
1444
|
+
fs.writeFileSync(path.resolve(folder, `${name}.widget.tsx`), [
|
|
1445
|
+
`import { FC } from 'react'`,
|
|
1446
|
+
`import styles from './${lowerCase}.widget.module.css'`,
|
|
1447
|
+
``,
|
|
1448
|
+
`export type ${upperCase}WidgetProps = {}`,
|
|
1449
|
+
``,
|
|
1450
|
+
`export const ${upperCase}Widget: FC<${upperCase}WidgetProps> = ({ }) => {`,
|
|
1451
|
+
`\treturn undefined`,
|
|
1452
|
+
`}`
|
|
1453
|
+
].join('\n'))
|
|
1454
|
+
|
|
1455
|
+
fs.writeFileSync(path.resolve(folder, `${name}.widget.module.css`), [
|
|
1456
|
+
''
|
|
1457
|
+
].join('\n'))
|
|
1458
|
+
|
|
1459
|
+
fs.writeFileSync(path.resolve(folder, `index.ts`), [
|
|
1460
|
+
`export * from './${name}.widget'`
|
|
1461
|
+
].join('\n'))
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
if (command === 'w') {
|
|
1465
|
+
let rootPath = undefined
|
|
1466
|
+
if (entityName.includes('/')) {
|
|
1467
|
+
const splitData = entityName.split('/')
|
|
1468
|
+
entityName = splitData.pop()
|
|
1469
|
+
rootPath = splitData.join('/')
|
|
1470
|
+
}
|
|
1471
|
+
generateWidget(entityName, rootPath)
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
function generateLayoutedWidget(name, rootPath) {
|
|
1475
|
+
const [lowerCase, upperCase] = camelizeVariants(name)
|
|
1476
|
+
|
|
1477
|
+
const widgetsPath = config.paths.widgets
|
|
1478
|
+
const root = findProjectRoot()
|
|
1479
|
+
|
|
1480
|
+
const folder = path.resolve(root, `${config.coreFolder}${widgetsPath}`, rootPath ?? '.', name)
|
|
1481
|
+
fs.mkdirSync(folder, { recursive: true })
|
|
1482
|
+
|
|
1483
|
+
fs.writeFileSync(path.resolve(folder, `${name}.headless-widget.tsx`), [
|
|
1484
|
+
`import { FC } from 'react'`,
|
|
1485
|
+
`import { ${upperCase}WidgetLayoutProps } from './${name}.widget-layout'`,
|
|
1486
|
+
``,
|
|
1487
|
+
`export type ${upperCase}HeadlessWidgetProps = {`,
|
|
1488
|
+
`\tLayout: FC<${upperCase}WidgetLayoutProps>,`,
|
|
1489
|
+
`}`,
|
|
1490
|
+
``,
|
|
1491
|
+
`export const ${upperCase}HeadlessWidget: FC<${upperCase}HeadlessWidgetProps> = ({ Layout }) => {`,
|
|
1492
|
+
`\treturn <Layout />`,
|
|
1493
|
+
`}`
|
|
1494
|
+
].join('\n'))
|
|
1495
|
+
|
|
1496
|
+
fs.writeFileSync(path.resolve(folder, `${name}.widget-layout.module.css`), [
|
|
1497
|
+
''
|
|
1498
|
+
].join('\n'))
|
|
1499
|
+
|
|
1500
|
+
fs.writeFileSync(path.resolve(folder, `${name}.widget-layout.tsx`), [
|
|
1501
|
+
`import { FC } from 'react'`,
|
|
1502
|
+
`import styles from './${name}.widget-layout.module.css'`,
|
|
1503
|
+
``,
|
|
1504
|
+
`export type ${upperCase}WidgetLayoutProps = {}`,
|
|
1505
|
+
``,
|
|
1506
|
+
`export const ${upperCase}WidgetLayout: FC<${upperCase}WidgetLayoutProps> = ({}) => {`,
|
|
1507
|
+
`\treturn undefined`,
|
|
1508
|
+
`}`
|
|
1509
|
+
].join('\n'))
|
|
1510
|
+
|
|
1511
|
+
fs.writeFileSync(path.resolve(folder, `${name}.widget.tsx`), [
|
|
1512
|
+
`import { FC } from 'react'`,
|
|
1513
|
+
`import { ${upperCase}HeadlessWidget, ${upperCase}HeadlessWidgetProps } from './${name}.headless-widget'`,
|
|
1514
|
+
`import { ${upperCase}WidgetLayout } from './${name}.widget-layout'`,
|
|
1515
|
+
``,
|
|
1516
|
+
`export type ${upperCase}WidgetProps = Omit<${upperCase}HeadlessWidgetProps, 'Layout'>`,
|
|
1517
|
+
``,
|
|
1518
|
+
`export const ${upperCase}Widget: FC<${upperCase}WidgetProps> = (props) => {`,
|
|
1519
|
+
`\treturn <${upperCase}HeadlessWidget Layout={${upperCase}WidgetLayout} {...props}/>`,
|
|
1520
|
+
`}`
|
|
1521
|
+
].join('\n'))
|
|
1522
|
+
|
|
1523
|
+
fs.writeFileSync(path.resolve(folder, `index.ts`), [
|
|
1524
|
+
`export * from './${name}.widget'`
|
|
1525
|
+
].join('\n'))
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
|
|
1529
|
+
if (command === 'lw') {
|
|
1530
|
+
let rootPath = undefined
|
|
1531
|
+
if (entityName.includes('/')) {
|
|
1532
|
+
const splitData = entityName.split('/')
|
|
1533
|
+
entityName = splitData.pop()
|
|
1534
|
+
rootPath = splitData.join('/')
|
|
1535
|
+
}
|
|
1536
|
+
generateLayoutedWidget(entityName, rootPath)
|
|
1537
|
+
}
|