sonamu 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/.pnp.cjs +15552 -0
  2. package/.pnp.loader.mjs +285 -0
  3. package/.vscode/extensions.json +6 -0
  4. package/.vscode/settings.json +9 -0
  5. package/.yarnrc.yml +5 -0
  6. package/dist/bin/cli.d.ts +2 -0
  7. package/dist/bin/cli.d.ts.map +1 -0
  8. package/dist/bin/cli.js +123 -0
  9. package/dist/bin/cli.js.map +1 -0
  10. package/dist/index.js +34 -0
  11. package/package.json +60 -0
  12. package/src/api/caster.ts +72 -0
  13. package/src/api/code-converters.ts +552 -0
  14. package/src/api/context.ts +20 -0
  15. package/src/api/decorators.ts +63 -0
  16. package/src/api/index.ts +5 -0
  17. package/src/api/init.ts +128 -0
  18. package/src/bin/cli.ts +115 -0
  19. package/src/database/base-model.ts +287 -0
  20. package/src/database/db.ts +95 -0
  21. package/src/database/knex-plugins/knex-on-duplicate-update.ts +41 -0
  22. package/src/database/upsert-builder.ts +231 -0
  23. package/src/exceptions/error-handler.ts +29 -0
  24. package/src/exceptions/so-exceptions.ts +91 -0
  25. package/src/index.ts +17 -0
  26. package/src/shared/web.shared.ts.txt +119 -0
  27. package/src/smd/migrator.ts +1462 -0
  28. package/src/smd/smd-manager.ts +141 -0
  29. package/src/smd/smd-utils.ts +266 -0
  30. package/src/smd/smd.ts +533 -0
  31. package/src/syncer/index.ts +1 -0
  32. package/src/syncer/syncer.ts +1283 -0
  33. package/src/templates/base-template.ts +19 -0
  34. package/src/templates/generated.template.ts +247 -0
  35. package/src/templates/generated_http.template.ts +114 -0
  36. package/src/templates/index.ts +1 -0
  37. package/src/templates/init_enums.template.ts +71 -0
  38. package/src/templates/init_generated.template.ts +44 -0
  39. package/src/templates/init_types.template.ts +38 -0
  40. package/src/templates/model.template.ts +168 -0
  41. package/src/templates/model_test.template.ts +39 -0
  42. package/src/templates/service.template.ts +263 -0
  43. package/src/templates/smd.template.ts +49 -0
  44. package/src/templates/view_enums_buttonset.template.ts +34 -0
  45. package/src/templates/view_enums_dropdown.template.ts +67 -0
  46. package/src/templates/view_enums_select.template.ts +60 -0
  47. package/src/templates/view_form.template.ts +397 -0
  48. package/src/templates/view_id_all_select.template.ts +34 -0
  49. package/src/templates/view_id_async_select.template.ts +113 -0
  50. package/src/templates/view_list.template.ts +652 -0
  51. package/src/templates/view_list_columns.template.ts +59 -0
  52. package/src/templates/view_search_input.template.ts +67 -0
  53. package/src/testing/fixture-manager.ts +271 -0
  54. package/src/types/types.ts +668 -0
  55. package/src/typings/knex.d.ts +24 -0
  56. package/src/utils/controller.ts +21 -0
  57. package/src/utils/lodash-able.ts +11 -0
  58. package/src/utils/model.ts +33 -0
  59. package/src/utils/utils.ts +28 -0
  60. package/tsconfig.json +47 -0
@@ -0,0 +1,1283 @@
1
+ import path, { dirname } from "path";
2
+ import { globAsync, importMultiple } from "../utils/utils";
3
+ import fs, {
4
+ existsSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ readJSON,
8
+ writeFile,
9
+ writeFileSync,
10
+ writeJSON,
11
+ } from "fs-extra";
12
+ import crypto from "crypto";
13
+ import equal from "fast-deep-equal";
14
+ import { differenceWith, groupBy, isEqual, uniq } from "lodash";
15
+ import { camelize } from "inflection";
16
+ import { SMDManager } from "../smd/smd-manager";
17
+ import * as ts from "typescript";
18
+ import {
19
+ ApiParam,
20
+ ApiParamType,
21
+ isBelongsToOneRelationProp,
22
+ isBigIntegerProp,
23
+ isBooleanProp,
24
+ isDateProp,
25
+ isDateTimeProp,
26
+ isDecimalProp,
27
+ isDoubleProp,
28
+ isEnumProp,
29
+ isFloatProp,
30
+ isIntegerProp,
31
+ isJsonProp,
32
+ isOneToOneRelationProp,
33
+ isRelationProp,
34
+ isStringProp,
35
+ isTextProp,
36
+ isTimeProp,
37
+ isTimestampProp,
38
+ isUuidProp,
39
+ isVirtualProp,
40
+ SMDProp,
41
+ SMDPropNode,
42
+ SQLDateTimeString,
43
+ } from "../types/types";
44
+ import {
45
+ ApiDecoratorOptions,
46
+ registeredApis,
47
+ ExtendedApi,
48
+ } from "../api/decorators";
49
+ import { z } from "zod";
50
+ import chalk from "chalk";
51
+ import {
52
+ TemplateKey,
53
+ PathAndCode,
54
+ TemplateOptions,
55
+ GenerateOptions,
56
+ RenderingNode,
57
+ } from "../types/types";
58
+ import {
59
+ AlreadyProcessedException,
60
+ BadRequestException,
61
+ ServiceUnavailableException,
62
+ } from "../exceptions/so-exceptions";
63
+ import { wrapIf } from "../utils/lodash-able";
64
+ import { getTextTypeLength } from "../api/code-converters";
65
+ import { Template } from "../templates/base-template";
66
+ import { Template__generated } from "../templates/generated.template";
67
+ import { Template__init_enums } from "../templates/init_enums.template";
68
+ import { Template__init_generated } from "../templates/init_generated.template";
69
+ import { Template__init_types } from "../templates/init_types.template";
70
+ import { Template__smd } from "../templates/smd.template";
71
+ import { Template__model } from "../templates/model.template";
72
+ import { Template__model_test } from "../templates/model_test.template";
73
+ import { Template__service } from "../templates/service.template";
74
+ import { Template__view_form } from "../templates/view_form.template";
75
+ import { Template__view_list } from "../templates/view_list.template";
76
+ import prettier from "prettier";
77
+ import { Template__view_id_all_select } from "../templates/view_id_all_select.template";
78
+ import { Template__view_id_async_select } from "../templates/view_id_async_select.template";
79
+ import { Template__view_enums_dropdown } from "../templates/view_enums_dropdown.template";
80
+ import { Template__view_enums_select } from "../templates/view_enums_select.template";
81
+ import { Template__view_enums_buttonset } from "../templates/view_enums_buttonset.template";
82
+ import { Template__view_search_input } from "../templates/view_search_input.template";
83
+ import { Template__view_list_columns } from "../templates/view_list_columns.template";
84
+ import { Template__generated_http } from "../templates/generated_http.template";
85
+
86
+ type SyncerConfig = {
87
+ appRootPath: string;
88
+ checksumsPath: string;
89
+ targets: string[];
90
+ };
91
+ type FileType = "model" | "types" | "enums" | "smd" | "generated";
92
+ type GlobPattern = {
93
+ [key in FileType]: string;
94
+ };
95
+ type PathAndChecksum = {
96
+ path: string;
97
+ checksum: string;
98
+ };
99
+ type DiffGroups = {
100
+ [key in FileType]: string[];
101
+ };
102
+ export type RenderedTemplate = {
103
+ target: string;
104
+ path: string;
105
+ body: string;
106
+ importKeys: string[];
107
+ customHeaders?: string[];
108
+ preTemplates?: {
109
+ key: TemplateKey;
110
+ options: TemplateOptions[TemplateKey];
111
+ }[];
112
+ };
113
+
114
+ export class Syncer {
115
+ private static instance: Syncer;
116
+ public static getInstance(config?: Partial<SyncerConfig>) {
117
+ if (this.instance && config !== undefined) {
118
+ throw new Error("Syncer has already configured.");
119
+ }
120
+ return this.instance ?? (this.instance = new this(config));
121
+ }
122
+
123
+ config: SyncerConfig;
124
+ apis: {
125
+ typeParameters: ApiParamType.TypeParam[];
126
+ parameters: ApiParam[];
127
+ returnType: ApiParamType;
128
+ modelName: string;
129
+ methodName: string;
130
+ path: string;
131
+ options: ApiDecoratorOptions;
132
+ }[] = [];
133
+ types: { [typeName: string]: z.ZodObject<any> } = {};
134
+
135
+ private constructor(config?: Partial<SyncerConfig>) {
136
+ const appRootPath =
137
+ config?.appRootPath ?? path.resolve(__dirname, "../../");
138
+ this.config = {
139
+ appRootPath,
140
+ checksumsPath: `${appRootPath}/api/.tf-checksum`,
141
+ targets: ["web"],
142
+ ...config,
143
+ };
144
+ }
145
+
146
+ async sync(): Promise<void> {
147
+ // 트리거와 무관하게 shared 분배
148
+ await Promise.all(
149
+ this.config.targets.map(async (target) => {
150
+ const srcCodePath = path
151
+ .join(__dirname, `../shared/${target}.shared.ts.txt`)
152
+ .replace("/dist/", "/src/");
153
+ if (!existsSync(srcCodePath)) {
154
+ return;
155
+ }
156
+
157
+ const dstCodePath = path.join(
158
+ this.config.appRootPath,
159
+ target,
160
+ "src/services/sonamu.shared.ts"
161
+ );
162
+
163
+ const srcChecksum = await this.getChecksumOfFile(srcCodePath);
164
+ const dstChecksum = await (async () => {
165
+ if (existsSync(dstCodePath) === false) {
166
+ return "";
167
+ }
168
+ return this.getChecksumOfFile(dstCodePath);
169
+ })();
170
+
171
+ if (srcChecksum === dstChecksum) {
172
+ return;
173
+ }
174
+ writeFileSync(dstCodePath, readFileSync(srcCodePath));
175
+ })
176
+ );
177
+
178
+ // 현재 checksums
179
+ const currentChecksums = await this.getCurrentChecksums();
180
+ // 이전 checksums
181
+ const previousChecksums = await this.getPreviousChecksums();
182
+
183
+ // 비교
184
+ const isSame = equal(currentChecksums, previousChecksums);
185
+ if (isSame) {
186
+ const msg = "Every files are synced!";
187
+ const margin = (process.stdout.columns - msg.length) / 2;
188
+ console.log(
189
+ chalk.black.bgGreen(" ".repeat(margin) + msg + " ".repeat(margin))
190
+ );
191
+ return;
192
+ }
193
+
194
+ // 변경된 파일 찾기
195
+ const diff = differenceWith(currentChecksums, previousChecksums, isEqual);
196
+ const diffFiles = diff.map((r) => r.path);
197
+ console.log("Changed Files: ", diffFiles);
198
+
199
+ // 다른 부분 찾아 액션
200
+ const diffGroups = groupBy(diffFiles, (r) => {
201
+ const matched = r.match(/\.(model|types|enums|smd|generated)\.[tj]s/);
202
+ return matched![1];
203
+ }) as unknown as DiffGroups;
204
+
205
+ // 변경된 파일들을 타입별로 분리하여 각 타입별 액션 처리
206
+ const diffTypes = Object.keys(diffGroups);
207
+
208
+ // 트리거: smd
209
+ // 액션: 스키마 생성
210
+ if (diffTypes.includes("smd")) {
211
+ console.log("// 액션: 스키마 생성");
212
+ const smdIds = this.getSMDIdFromPath(diffGroups["smd"]);
213
+ await this.actionGenerateSchemas(smdIds);
214
+ }
215
+
216
+ // 트리거: types, enums, generated 변경시
217
+ // 액션: 파일 싱크 types, enums, generated
218
+ if (
219
+ diffTypes.includes("types") ||
220
+ diffTypes.includes("enums") ||
221
+ diffTypes.includes("generated")
222
+ ) {
223
+ console.log("// 액션: 파일 싱크 types / enums / generated");
224
+
225
+ const tsPaths = [
226
+ ...(diffGroups["types"] ?? []),
227
+ ...(diffGroups["enums"] ?? []),
228
+ ...(diffGroups["generated"] ?? []),
229
+ ].map((p) => p.replace("/dist/", "/src/").replace(".js", ".ts"));
230
+ await this.actionSyncFilesToTargets(tsPaths);
231
+ }
232
+
233
+ // 트리거: model
234
+ if (diffTypes.includes("model")) {
235
+ const smdIds = this.getSMDIdFromPath(diffGroups["model"]);
236
+
237
+ console.log("// 액션: 서비스 생성");
238
+ await this.actionGenerateServices(smdIds);
239
+
240
+ console.log("// 액션: HTTP파일 생성");
241
+ await this.actionGenerateHttps(smdIds);
242
+ }
243
+
244
+ // 저장
245
+ await this.saveChecksums(currentChecksums);
246
+ }
247
+
248
+ getSMDIdFromPath(filePaths: string[]): string[] {
249
+ return filePaths.map((p) => {
250
+ const matched = p.match(/application\/(.+)\//);
251
+ return camelize(matched![1].replace(/\-/g, "_"));
252
+ });
253
+ }
254
+
255
+ async actionGenerateSchemas(smdIds: string[]): Promise<string[]> {
256
+ return (
257
+ await Promise.all(
258
+ smdIds.map(async (smdId) =>
259
+ this.generateTemplate(
260
+ "generated",
261
+ {
262
+ smdId,
263
+ },
264
+ {
265
+ overwrite: true,
266
+ }
267
+ )
268
+ )
269
+ )
270
+ )
271
+ .flat()
272
+ .flat();
273
+ }
274
+
275
+ async actionGenerateServices(smdIds: string[]): Promise<string[]> {
276
+ return (
277
+ await Promise.all(
278
+ smdIds.map(async (smdId) =>
279
+ this.generateTemplate(
280
+ "service",
281
+ {
282
+ smdId,
283
+ },
284
+ {
285
+ overwrite: true,
286
+ }
287
+ )
288
+ )
289
+ )
290
+ )
291
+ .flat()
292
+ .flat();
293
+ }
294
+
295
+ async actionGenerateHttps(smdIds: string[]): Promise<string[]> {
296
+ return (
297
+ await Promise.all(
298
+ smdIds.map(async (smdId) =>
299
+ this.generateTemplate(
300
+ "generated_http",
301
+ {
302
+ smdId,
303
+ },
304
+ {
305
+ overwrite: true,
306
+ }
307
+ )
308
+ )
309
+ )
310
+ )
311
+ .flat()
312
+ .flat();
313
+ }
314
+
315
+ async copyFileWithReplaceCoreToShared(fromPath: string, toPath: string) {
316
+ if (!existsSync(fromPath)) {
317
+ return;
318
+ }
319
+
320
+ const oldFileContent = readFileSync(fromPath).toString();
321
+ const newFileContent = oldFileContent
322
+ .replace(/@sonamu\/core/g, "../sonamu.shared")
323
+ .replace(
324
+ /\/\* BEGIN- Server-side Only \*\/[\s\S]*\/\* END Server-side Only \*\/\n*/g,
325
+ ""
326
+ );
327
+ return writeFile(toPath, newFileContent);
328
+ }
329
+
330
+ async actionSyncFilesToTargets(tsPaths: string[]): Promise<string[]> {
331
+ return (
332
+ await Promise.all(
333
+ this.config.targets.map(async (target) =>
334
+ Promise.all(
335
+ tsPaths.map(async (src) => {
336
+ const dst = src
337
+ .replace("/api/", `/${target}/`)
338
+ .replace("/application/", "/services/");
339
+ const dir = dirname(dst);
340
+ if (!existsSync(dir)) {
341
+ mkdirSync(dir, { recursive: true });
342
+ }
343
+ await this.copyFileWithReplaceCoreToShared(src, dst);
344
+ return dst;
345
+ })
346
+ )
347
+ )
348
+ )
349
+ ).flat();
350
+ }
351
+
352
+ async getCurrentChecksums(): Promise<PathAndChecksum[]> {
353
+ const PatternGroup: GlobPattern = {
354
+ /* TS 체크 */
355
+ types: this.config.appRootPath + "/api/src/application/**/*.types.ts",
356
+ enums: this.config.appRootPath + "/api/src/application/**/*.enums.ts",
357
+ generated:
358
+ this.config.appRootPath + "/api/src/application/**/*.generated.ts",
359
+ /* compiled-JS 체크 */
360
+ model: this.config.appRootPath + "/api/dist/application/**/*.model.js",
361
+ smd: this.config.appRootPath + "/api/dist/application/**/*.smd.js",
362
+ };
363
+
364
+ const filePaths = (
365
+ await Promise.all(
366
+ Object.entries(PatternGroup).map(async ([_fileType, pattern]) => {
367
+ return globAsync(pattern);
368
+ })
369
+ )
370
+ )
371
+ .flat()
372
+ .sort();
373
+
374
+ const fileChecksums: {
375
+ path: string;
376
+ checksum: string;
377
+ }[] = await Promise.all(
378
+ filePaths.map(async (filePath) => {
379
+ return {
380
+ path: filePath,
381
+ checksum: await this.getChecksumOfFile(filePath),
382
+ };
383
+ })
384
+ );
385
+ return fileChecksums;
386
+ }
387
+
388
+ async getPreviousChecksums(): Promise<PathAndChecksum[]> {
389
+ if (existsSync(this.config.checksumsPath) === false) {
390
+ return [];
391
+ }
392
+
393
+ const previousChecksums = (await readJSON(
394
+ this.config.checksumsPath
395
+ )) as PathAndChecksum[];
396
+ return previousChecksums;
397
+ }
398
+
399
+ async saveChecksums(checksums: PathAndChecksum[]): Promise<void> {
400
+ await writeJSON(this.config.checksumsPath, checksums);
401
+ console.debug("checksum saved", this.config.checksumsPath);
402
+ }
403
+
404
+ async getChecksumOfFile(filePath: string): Promise<string> {
405
+ return new Promise<string>((resolve, reject) => {
406
+ const hash = crypto.createHash("sha1");
407
+ const input = fs.createReadStream(filePath);
408
+ input.on("error", reject);
409
+ input.on("data", function (chunk: any) {
410
+ hash.update(chunk);
411
+ });
412
+ input.on("close", function () {
413
+ resolve(hash.digest("hex"));
414
+ });
415
+ });
416
+ }
417
+
418
+ async readApisFromFile(filePath: string) {
419
+ const sourceFile = ts.createSourceFile(
420
+ filePath,
421
+ readFileSync(filePath).toString(),
422
+ ts.ScriptTarget.Latest
423
+ );
424
+
425
+ const methods: Omit<ExtendedApi, "path" | "options">[] = [];
426
+ let modelName: string = "UnknownModel";
427
+ let methodName: string = "unknownMethod";
428
+ const visitor = (node: ts.Node) => {
429
+ if (ts.isClassDeclaration(node)) {
430
+ if (node.name && ts.isIdentifier(node.name)) {
431
+ modelName = node.name.escapedText.toString().replace(/Class$/, "");
432
+ }
433
+ }
434
+ if (ts.isMethodDeclaration(node)) {
435
+ if (ts.isIdentifier(node.name)) {
436
+ methodName = node.name.escapedText.toString();
437
+ }
438
+
439
+ const typeParameters: ApiParamType.TypeParam[] = (
440
+ node.typeParameters ?? []
441
+ ).map((typeParam) => {
442
+ const tp = typeParam as ts.TypeParameterDeclaration;
443
+
444
+ return {
445
+ t: "type-param",
446
+ id: tp.name.escapedText.toString(),
447
+ constraint: tp.constraint
448
+ ? this.resolveTypeNode(tp.constraint)
449
+ : undefined,
450
+ };
451
+ });
452
+ const parameters: ApiParam[] = node.parameters.map(
453
+ (paramDec, index) => {
454
+ const defaultDef = this.printNode(paramDec.initializer, sourceFile);
455
+
456
+ return this.resolveParamDec(
457
+ {
458
+ name: paramDec.name,
459
+ type: paramDec.type as ts.TypeNode,
460
+ optional:
461
+ paramDec.questionToken !== undefined ||
462
+ paramDec.initializer !== undefined,
463
+ defaultDef,
464
+ },
465
+ index
466
+ );
467
+ }
468
+ );
469
+ const returnType = this.resolveTypeNode(node.type!);
470
+
471
+ methods.push({
472
+ modelName,
473
+ methodName,
474
+ typeParameters,
475
+ parameters,
476
+ returnType,
477
+ });
478
+ }
479
+ ts.forEachChild(node, visitor);
480
+ };
481
+ visitor(sourceFile);
482
+
483
+ if (methods.length === 0) {
484
+ return [];
485
+ }
486
+
487
+ // 현재 파일의 등록된 API 필터
488
+ const currentModelApis = registeredApis.filter((api) => {
489
+ return methods.find(
490
+ (method) =>
491
+ method.modelName === api.modelName &&
492
+ method.methodName === api.methodName
493
+ );
494
+ });
495
+
496
+ // 등록된 API에 현재 메소드 타입 정보 확장
497
+ const extendedApis = currentModelApis.map((api) => {
498
+ const foundMethod = methods.find(
499
+ (method) =>
500
+ method.modelName === api.modelName &&
501
+ method.methodName === api.methodName
502
+ );
503
+ return {
504
+ ...api,
505
+ typeParameters: foundMethod!.typeParameters,
506
+ parameters: foundMethod!.parameters,
507
+ returnType: foundMethod!.returnType,
508
+ };
509
+ });
510
+ return extendedApis;
511
+ }
512
+
513
+ resolveTypeNode(typeNode: ts.TypeNode): ApiParamType {
514
+ switch (typeNode?.kind) {
515
+ case ts.SyntaxKind.AnyKeyword:
516
+ return "any";
517
+ case ts.SyntaxKind.UnknownKeyword:
518
+ return "unknown";
519
+ case ts.SyntaxKind.StringKeyword:
520
+ return "string";
521
+ case ts.SyntaxKind.NumberKeyword:
522
+ return "number";
523
+ case ts.SyntaxKind.BooleanKeyword:
524
+ return "boolean";
525
+ case ts.SyntaxKind.UndefinedKeyword:
526
+ return "undefined";
527
+ case ts.SyntaxKind.NullKeyword:
528
+ return "null";
529
+ case ts.SyntaxKind.VoidKeyword:
530
+ return "void";
531
+ case ts.SyntaxKind.LiteralType:
532
+ const literal = (typeNode as ts.LiteralTypeNode).literal;
533
+ if (ts.isStringLiteral(literal)) {
534
+ return {
535
+ t: "string-literal",
536
+ value: literal.text,
537
+ };
538
+ } else if (ts.isNumericLiteral(literal)) {
539
+ return {
540
+ t: "numeric-literal",
541
+ value: Number(literal.text),
542
+ };
543
+ } else {
544
+ if (literal.kind === ts.SyntaxKind.NullKeyword) {
545
+ return "null";
546
+ } else if (literal.kind === ts.SyntaxKind.UndefinedKeyword) {
547
+ return "undefined";
548
+ } else if (literal.kind === ts.SyntaxKind.TrueKeyword) {
549
+ return "true";
550
+ } else if (literal.kind === ts.SyntaxKind.FalseKeyword) {
551
+ return "false";
552
+ }
553
+ throw new Error("알 수 없는 리터럴");
554
+ }
555
+ case ts.SyntaxKind.ArrayType:
556
+ const arrNode = typeNode as ts.ArrayTypeNode;
557
+ return {
558
+ t: "array",
559
+ elementsType: this.resolveTypeNode(arrNode.elementType),
560
+ };
561
+ case ts.SyntaxKind.TypeLiteral:
562
+ const literalNode = typeNode as ts.TypeLiteralNode;
563
+ return {
564
+ t: "object",
565
+ props: literalNode.members.map((member) => {
566
+ if (ts.isIndexSignatureDeclaration(member)) {
567
+ const res = this.resolveParamDec({
568
+ name: member.parameters[0].name as ts.Identifier,
569
+ type: member.parameters[0].type as ts.TypeNode,
570
+ });
571
+
572
+ return this.resolveParamDec({
573
+ name: {
574
+ escapedText: `[${res.name}${res.optional ? "?" : ""}: ${
575
+ res.type
576
+ }]`,
577
+ } as ts.Identifier,
578
+ type: member.type as ts.TypeNode,
579
+ });
580
+ } else {
581
+ return this.resolveParamDec({
582
+ name: (member as ts.PropertySignature).name as ts.Identifier,
583
+ type: (member as ts.PropertySignature).type as ts.TypeNode,
584
+ });
585
+ }
586
+ }),
587
+ };
588
+ case ts.SyntaxKind.TypeReference:
589
+ return {
590
+ t: "ref",
591
+ id: (
592
+ (typeNode as ts.TypeReferenceNode).typeName as ts.Identifier
593
+ ).escapedText.toString(),
594
+ args: (typeNode as ts.TypeReferenceNode).typeArguments?.map(
595
+ (typeArg) => this.resolveTypeNode(typeArg)
596
+ ),
597
+ };
598
+ case ts.SyntaxKind.UnionType:
599
+ return {
600
+ t: "union",
601
+ types: (typeNode as ts.UnionTypeNode).types.map((type) =>
602
+ this.resolveTypeNode(type)
603
+ ),
604
+ };
605
+ case ts.SyntaxKind.IntersectionType:
606
+ return {
607
+ t: "intersection",
608
+ types: (typeNode as ts.IntersectionTypeNode).types.map((type) =>
609
+ this.resolveTypeNode(type)
610
+ ),
611
+ };
612
+ case ts.SyntaxKind.IndexedAccessType:
613
+ return {
614
+ t: "indexed-access",
615
+ object: this.resolveTypeNode(
616
+ (typeNode as ts.IndexedAccessTypeNode).objectType
617
+ ),
618
+ index: this.resolveTypeNode(
619
+ (typeNode as ts.IndexedAccessTypeNode).indexType
620
+ ),
621
+ };
622
+ case ts.SyntaxKind.TupleType:
623
+ if (ts.isTupleTypeNode(typeNode)) {
624
+ return {
625
+ t: "tuple-type",
626
+ elements: typeNode.elements.map((elem) =>
627
+ this.resolveTypeNode(elem)
628
+ ),
629
+ };
630
+ }
631
+ break;
632
+ }
633
+
634
+ console.debug(typeNode);
635
+ throw new Error(`알 수 없는 SyntaxKind ${typeNode.kind}`);
636
+ }
637
+
638
+ resolveParamDec = (
639
+ paramDec: {
640
+ name: ts.BindingName;
641
+ type: ts.TypeNode;
642
+ optional?: boolean;
643
+ defaultDef?: string;
644
+ },
645
+ index: number = 0
646
+ ): ApiParam => {
647
+ const name = paramDec.name as ts.Identifier;
648
+ const type = this.resolveTypeNode(paramDec.type);
649
+
650
+ if (name === undefined) {
651
+ console.log({ name, type, paramDec });
652
+ }
653
+
654
+ return {
655
+ name: name.escapedText ? name.escapedText.toString() : `nonameAt${index}`,
656
+ type,
657
+ optional: paramDec.optional === true,
658
+ defaultDef: paramDec?.defaultDef,
659
+ };
660
+ };
661
+
662
+ printNode(
663
+ node: ts.Node | undefined,
664
+ sourceFile: ts.SourceFile
665
+ ): string | undefined {
666
+ if (node === undefined) {
667
+ return undefined;
668
+ }
669
+
670
+ const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
671
+ return printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
672
+ }
673
+
674
+ async autoloadApis(basePath: string) {
675
+ const pathPattern = path.join(
676
+ basePath,
677
+ "api/src/application/**/*.model.ts"
678
+ );
679
+ // console.debug(chalk.yellow(`autoload:APIs @ ${pathPattern}`));
680
+
681
+ const filePaths = await globAsync(pathPattern);
682
+ const result = await Promise.all(
683
+ filePaths.map((filePath) => this.readApisFromFile(filePath))
684
+ );
685
+ this.apis = result.flat();
686
+ return this.apis;
687
+ }
688
+
689
+ async autoloadModels(
690
+ basePath: string
691
+ ): Promise<{ [modelName: string]: unknown }> {
692
+ const pathPattern = path.join(
693
+ basePath,
694
+ "api/dist/application/**/*.model.js"
695
+ );
696
+ // console.debug(chalk.yellow(`autoload:models @ ${pathPattern}`));
697
+
698
+ const filePaths = await globAsync(pathPattern);
699
+ const modules = await importMultiple(filePaths);
700
+ const functions = modules
701
+ .map(({ imported }) => Object.entries(imported))
702
+ .flat();
703
+ return Object.fromEntries(
704
+ functions.filter(([name]) => name.endsWith("Model"))
705
+ );
706
+ }
707
+
708
+ async autoloadTypes(
709
+ basePath: string
710
+ ): Promise<{ [typeName: string]: z.ZodObject<any> }> {
711
+ if (Object.keys(this.types).length > 0) {
712
+ return this.types;
713
+ }
714
+
715
+ const pathPatterns = [
716
+ path.join(basePath, "api/dist/application/**/*.types.js"),
717
+ path.join(basePath, "api/dist/application/**/*.enums.js"),
718
+ path.join(basePath, "api/dist/application/**/*.generated.js"),
719
+ ];
720
+ // console.debug(chalk.magenta(`autoload:types @ ${pathPatterns.join("\n")}`));
721
+
722
+ const filePaths = (
723
+ await Promise.all(pathPatterns.map((pattern) => globAsync(pattern)))
724
+ ).flat();
725
+ const modules = await importMultiple(filePaths);
726
+ const functions = modules
727
+ .map(({ imported }) => Object.entries(imported))
728
+ .flat();
729
+ this.types = Object.fromEntries(
730
+ functions.filter(([, f]) => f instanceof z.ZodType)
731
+ ) as typeof this.types;
732
+ return this.types;
733
+ }
734
+
735
+ getTemplate(key: TemplateKey): Template {
736
+ if (key === "smd") {
737
+ return new Template__smd();
738
+ } else if (key === "init_enums") {
739
+ return new Template__init_enums();
740
+ } else if (key === "init_types") {
741
+ return new Template__init_types();
742
+ } else if (key === "init_generated") {
743
+ return new Template__init_generated();
744
+ } else if (key === "generated") {
745
+ return new Template__generated();
746
+ } else if (key === "generated_http") {
747
+ return new Template__generated_http();
748
+ } else if (key === "model") {
749
+ return new Template__model();
750
+ } else if (key === "model_test") {
751
+ return new Template__model_test();
752
+ } else if (key === "service") {
753
+ return new Template__service();
754
+ } else if (key === "view_list") {
755
+ return new Template__view_list();
756
+ } else if (key === "view_list_columns") {
757
+ return new Template__view_list_columns();
758
+ } else if (key === "view_search_input") {
759
+ return new Template__view_search_input();
760
+ } else if (key === "view_form") {
761
+ return new Template__view_form();
762
+ } else if (key === "view_id_all_select") {
763
+ return new Template__view_id_all_select();
764
+ } else if (key === "view_id_async_select") {
765
+ return new Template__view_id_async_select();
766
+ } else if (key === "view_enums_select") {
767
+ return new Template__view_enums_select();
768
+ } else if (key === "view_enums_dropdown") {
769
+ return new Template__view_enums_dropdown();
770
+ } else if (key === "view_enums_buttonset") {
771
+ return new Template__view_enums_buttonset();
772
+ } else {
773
+ throw new BadRequestException(`잘못된 템플릿 키 ${key}`);
774
+ }
775
+ }
776
+
777
+ async renderTemplate(
778
+ key: TemplateKey,
779
+ options: TemplateOptions[TemplateKey]
780
+ ): Promise<PathAndCode[]> {
781
+ const template: Template = this.getTemplate(key);
782
+
783
+ let extra: unknown[] = [];
784
+ if (key === "service" || key === "generated_http") {
785
+ // service 필요 정보 (API 리스트)
786
+ const smd = SMDManager.get(options.smdId);
787
+ const modelTsPath = `${path.resolve(
788
+ this.config.appRootPath,
789
+ "api/src/application"
790
+ )}/${smd.names.fs}/${smd.names.fs}.model.ts`;
791
+ extra = [await this.readApisFromFile(modelTsPath)];
792
+ } else if (key === "view_list" || key === "model") {
793
+ // view_list 필요 정보 (컬럼 노드, 리스트파라미터 노드)
794
+ const columnsNode = await this.getColumnsNode(options.smdId, "A");
795
+ const listParamsZodType = await this.getZodTypeById(
796
+ `${options.smdId}ListParams`
797
+ );
798
+ const listParamsNode = this.zodTypeToRenderingNode(listParamsZodType);
799
+ extra = [columnsNode, listParamsNode];
800
+ } else if (key === "view_form") {
801
+ // view_form 필요 정보 (세이브파라미터 노드)
802
+ const saveParamsZodType = await this.getZodTypeById(
803
+ `${options.smdId}SaveParams`
804
+ );
805
+ const saveParamsNode = this.zodTypeToRenderingNode(saveParamsZodType);
806
+ extra = [saveParamsNode];
807
+ }
808
+
809
+ const rendered = template.render(options, ...extra);
810
+ const resolved = this.resolveRenderedTemplate(key, rendered);
811
+
812
+ let preTemplateResolved: PathAndCode[] = [];
813
+ if (rendered.preTemplates) {
814
+ preTemplateResolved = (
815
+ await Promise.all(
816
+ rendered.preTemplates.map(({ key, options }) => {
817
+ return this.renderTemplate(key, options);
818
+ })
819
+ )
820
+ ).flat();
821
+ }
822
+
823
+ return [resolved, ...preTemplateResolved];
824
+ }
825
+
826
+ resolveRenderedTemplate(
827
+ key: TemplateKey,
828
+ result: RenderedTemplate
829
+ ): PathAndCode {
830
+ const { target, path: filePath, body, importKeys, customHeaders } = result;
831
+
832
+ // import 할 대상의 대상 path 추출
833
+ const importDefs = importKeys
834
+ .reduce(
835
+ (r, importKey) => {
836
+ const modulePath = SMDManager.getModulePath(importKey);
837
+ let importPath = modulePath;
838
+ if (modulePath.includes("/")) {
839
+ importPath = wrapIf(
840
+ path.relative(path.dirname(filePath), modulePath),
841
+ (p) => [p.startsWith(".") === false, "./" + p]
842
+ );
843
+ }
844
+
845
+ // 같은 파일에서 import 하는 경우 keys 로 나열 처리
846
+ const existsOne = r.find(
847
+ (importDef) => importDef.from === importPath
848
+ );
849
+ if (existsOne) {
850
+ existsOne.keys = uniq(existsOne.keys.concat(importKey));
851
+ } else {
852
+ r.push({
853
+ keys: [importKey],
854
+ from: importPath,
855
+ });
856
+ }
857
+ return r;
858
+ },
859
+ [] as {
860
+ keys: string[];
861
+ from: string;
862
+ }[]
863
+ )
864
+ // 셀프 참조 방지
865
+ .filter(
866
+ (importDef) =>
867
+ filePath.endsWith(importDef.from.replace("./", "") + ".ts") === false
868
+ );
869
+
870
+ // 커스텀 헤더 포함하여 헤더 생성
871
+ const header = [
872
+ ...(customHeaders ?? []),
873
+ ...importDefs.map(
874
+ (importDef) =>
875
+ `import { ${importDef.keys.join(", ")} } from '${importDef.from}'`
876
+ ),
877
+ ].join("\n");
878
+
879
+ const formatted =
880
+ key === "generated_http"
881
+ ? [header, body].join("\n\n")
882
+ : prettier.format([header, body].join("\n\n"), {
883
+ parser: "typescript",
884
+ });
885
+
886
+ return {
887
+ path: target + "/" + filePath,
888
+ code: formatted,
889
+ };
890
+ }
891
+
892
+ async writeCodeToPath(pathAndCode: PathAndCode): Promise<string[]> {
893
+ const { appRootPath, targets } = this.config;
894
+ const filePath = `${appRootPath}/${pathAndCode.path}`;
895
+
896
+ const dstFilePaths = uniq(
897
+ targets.map((target) => filePath.replace("/:target/", `/${target}/`))
898
+ );
899
+ return await Promise.all(
900
+ dstFilePaths.map(async (dstFilePath) => {
901
+ const dir = path.dirname(dstFilePath);
902
+ if (existsSync(dir) === false) {
903
+ mkdirSync(dir, { recursive: true });
904
+ }
905
+ writeFileSync(dstFilePath, pathAndCode.code);
906
+ console.log("GENERATED ", chalk.blue(dstFilePath));
907
+ return dstFilePath;
908
+ })
909
+ );
910
+ }
911
+
912
+ async generateTemplate(
913
+ key: TemplateKey,
914
+ templateOptions: any,
915
+ _generateOptions?: GenerateOptions
916
+ ) {
917
+ const generateOptions = {
918
+ overwrite: false,
919
+ ..._generateOptions,
920
+ };
921
+
922
+ // 키 children
923
+ let keys: TemplateKey[] = [key];
924
+ if (key === "smd") {
925
+ keys = ["smd", "init_enums", "init_generated", "init_types"];
926
+ }
927
+
928
+ // 템플릿 렌더
929
+ const pathAndCodes = (
930
+ await Promise.all(
931
+ keys.map(async (key) => {
932
+ return this.renderTemplate(key, templateOptions);
933
+ })
934
+ )
935
+ ).flat();
936
+
937
+ /*
938
+ overwrite가 true일 때
939
+ - 생각하지 않고 그냥 다 덮어씀
940
+ overwrite가 false일 때
941
+ - 옵션1 (현재구현): 그냥 파일 하나라도 있으면 코드 생성 안함
942
+ - 옵션2 : 있는 파일은 전부 그대로 두고 없는 파일만 싹 생성함
943
+ - 옵션3 : 메인 파일만 그대로 두고, 파생 파일은 전부 생성함 => 이게 맞지 않나?
944
+ */
945
+
946
+ let filteredPathAndCodes: PathAndCode[] = [];
947
+ if (generateOptions.overwrite === true) {
948
+ filteredPathAndCodes = pathAndCodes;
949
+ } else {
950
+ filteredPathAndCodes = pathAndCodes.filter((pathAndCode, index) => {
951
+ if (index === 0) {
952
+ const { appRootPath, targets } = this.config;
953
+ const filePath = `${appRootPath}/${pathAndCode.path}`;
954
+ const dstFilePaths = targets.map((target) =>
955
+ filePath.replace("/:target/", `/${target}/`)
956
+ );
957
+ return dstFilePaths.every((dstPath) => existsSync(dstPath) === false);
958
+ } else {
959
+ return true;
960
+ }
961
+ });
962
+ if (filteredPathAndCodes.length === 0) {
963
+ throw new AlreadyProcessedException(
964
+ "이미 경로에 모든 파일이 존재합니다."
965
+ );
966
+ }
967
+ }
968
+
969
+ return Promise.all(
970
+ filteredPathAndCodes.map((pathAndCode) =>
971
+ this.writeCodeToPath(pathAndCode)
972
+ )
973
+ );
974
+ }
975
+
976
+ checkExists(
977
+ smdId: string,
978
+ enums: {
979
+ [name: string]: z.ZodEnum<any>;
980
+ }
981
+ ): Record<`${TemplateKey}${string}`, boolean> {
982
+ const keys: TemplateKey[] = TemplateKey.options;
983
+ const names = SMDManager.getNamesFromId(smdId);
984
+ const enumsKeys = Object.keys(enums).filter(
985
+ (name) => name !== names.constant
986
+ );
987
+
988
+ return keys.reduce((result, key) => {
989
+ const tpl = this.getTemplate(key);
990
+ if (key.startsWith("view_enums")) {
991
+ enumsKeys.map((componentId) => {
992
+ const { target, path: p } = tpl.getTargetAndPath(names, componentId);
993
+ result[`${key}__${componentId}`] = existsSync(
994
+ path.join(this.config.appRootPath, target, p)
995
+ );
996
+ });
997
+ return result;
998
+ }
999
+
1000
+ const { target, path: p } = tpl.getTargetAndPath(names);
1001
+ if (target.includes(":target")) {
1002
+ this.config.targets.map((t) => {
1003
+ result[`${key}__${t}`] = existsSync(
1004
+ path.join(this.config.appRootPath, target.replace(":target", t), p)
1005
+ );
1006
+ });
1007
+ } else {
1008
+ result[key] = existsSync(path.join(this.config.appRootPath, target, p));
1009
+ }
1010
+
1011
+ return result;
1012
+ }, {} as Record<`${TemplateKey}${string}`, boolean>);
1013
+ }
1014
+
1015
+ async getZodTypeById(zodTypeId: string): Promise<z.ZodTypeAny> {
1016
+ const modulePath = SMDManager.getModulePath(zodTypeId);
1017
+ const moduleAbsPath = path.join(
1018
+ this.config.appRootPath,
1019
+ "api",
1020
+ "dist",
1021
+ "application",
1022
+ modulePath + ".js"
1023
+ );
1024
+ const importPath = "./" + path.relative(__dirname, moduleAbsPath);
1025
+ const imported = await import(importPath);
1026
+
1027
+ if (!imported[zodTypeId]) {
1028
+ throw new Error(`존재하지 않는 zodTypeId ${zodTypeId}`);
1029
+ }
1030
+ return imported[zodTypeId].describe(zodTypeId);
1031
+ }
1032
+
1033
+ async propNodeToZodType(propNode: SMDPropNode): Promise<z.ZodTypeAny> {
1034
+ if (propNode.nodeType === "plain") {
1035
+ return this.propToZodType(propNode.prop);
1036
+ } else if (propNode.nodeType === "array") {
1037
+ if (propNode.prop === undefined) {
1038
+ throw new Error();
1039
+ } else if (propNode.children.length > 0) {
1040
+ return (
1041
+ await this.propNodeToZodType({
1042
+ ...propNode,
1043
+ nodeType: "object",
1044
+ })
1045
+ ).array();
1046
+ } else {
1047
+ const innerType = await this.propToZodType(propNode.prop);
1048
+ if (propNode.prop.nullable === true) {
1049
+ return z.array(innerType).nullable();
1050
+ } else {
1051
+ return z.array(innerType);
1052
+ }
1053
+ }
1054
+ } else if (propNode.nodeType === "object") {
1055
+ const obj = await propNode.children.reduce(
1056
+ async (promise, childPropNode) => {
1057
+ const result = await promise;
1058
+ result[childPropNode.prop!.name] = await this.propNodeToZodType(
1059
+ childPropNode
1060
+ );
1061
+ return result;
1062
+ },
1063
+ {} as any
1064
+ );
1065
+
1066
+ if (propNode.prop?.nullable === true) {
1067
+ return z.object(obj).nullable();
1068
+ } else {
1069
+ return z.object(obj);
1070
+ }
1071
+ } else {
1072
+ throw Error;
1073
+ }
1074
+ }
1075
+ async propToZodType(prop: SMDProp): Promise<z.ZodTypeAny> {
1076
+ let zodType: z.ZodTypeAny = z.unknown();
1077
+ if (isIntegerProp(prop)) {
1078
+ zodType = z.number().int();
1079
+ } else if (isBigIntegerProp(prop)) {
1080
+ zodType = z.bigint();
1081
+ } else if (isTextProp(prop)) {
1082
+ zodType = z.string().max(getTextTypeLength(prop.textType));
1083
+ } else if (isEnumProp(prop)) {
1084
+ zodType = await this.getZodTypeById(prop.id);
1085
+ } else if (isStringProp(prop)) {
1086
+ zodType = z.string().max(prop.length);
1087
+ } else if (isFloatProp(prop) || isDoubleProp(prop) || isDecimalProp(prop)) {
1088
+ zodType = z.number();
1089
+ } else if (isBooleanProp(prop)) {
1090
+ zodType = z.boolean();
1091
+ } else if (isDateProp(prop)) {
1092
+ zodType = z.string().length(10);
1093
+ } else if (isTimeProp(prop)) {
1094
+ zodType = z.string().length(8);
1095
+ } else if (isDateTimeProp(prop)) {
1096
+ zodType = SQLDateTimeString;
1097
+ } else if (isTimestampProp(prop)) {
1098
+ zodType = SQLDateTimeString;
1099
+ } else if (isJsonProp(prop)) {
1100
+ if (prop.as instanceof z.ZodType) {
1101
+ zodType = prop.as;
1102
+ } else {
1103
+ zodType = await this.getZodTypeById(prop.as.ref);
1104
+ }
1105
+ } else if (isUuidProp(prop)) {
1106
+ zodType = z.string().uuid();
1107
+ } else if (isVirtualProp(prop)) {
1108
+ if (prop.as instanceof z.ZodType) {
1109
+ zodType = prop.as;
1110
+ } else {
1111
+ zodType = await this.getZodTypeById(prop.as.ref);
1112
+ }
1113
+ } else if (isRelationProp(prop)) {
1114
+ if (
1115
+ isBelongsToOneRelationProp(prop) ||
1116
+ (isOneToOneRelationProp(prop) && prop.hasJoinColumn)
1117
+ ) {
1118
+ zodType = z.number().int();
1119
+ throw new Error("여기를 들어온다고?");
1120
+ }
1121
+ } else {
1122
+ throw new Error(`prop을 zodType으로 변환하는데 실패 ${prop}}`);
1123
+ }
1124
+
1125
+ if ((prop as { unsigned?: boolean }).unsigned) {
1126
+ zodType = (zodType as z.ZodNumber).nonnegative();
1127
+ }
1128
+ if (prop.nullable) {
1129
+ zodType = zodType.nullable();
1130
+ }
1131
+
1132
+ return zodType;
1133
+ }
1134
+
1135
+ resolveRenderType(
1136
+ key: string,
1137
+ zodType: z.ZodTypeAny
1138
+ ): RenderingNode["renderType"] {
1139
+ if (zodType instanceof z.ZodString) {
1140
+ if (key.includes("img") || key.includes("image")) {
1141
+ return "string-image";
1142
+ } else if (zodType.description === "SQLDateTimeString") {
1143
+ return "string-datetime";
1144
+ } else {
1145
+ return "string-plain";
1146
+ }
1147
+ } else if (zodType instanceof z.ZodNumber) {
1148
+ if (key === "id") {
1149
+ return "number-id";
1150
+ } else if (key.endsWith("_id")) {
1151
+ return "number-fk_id";
1152
+ } else {
1153
+ return "number-plain";
1154
+ }
1155
+ } else if (zodType instanceof z.ZodBoolean) {
1156
+ return "boolean";
1157
+ } else if (zodType instanceof z.ZodEnum) {
1158
+ return "enums";
1159
+ } else if (zodType instanceof z.ZodAny) {
1160
+ return "string-plain";
1161
+ } else {
1162
+ throw new Error(`타입 파싱 불가 ${key} ${zodType._def.typeName}`);
1163
+ }
1164
+ }
1165
+ zodTypeToRenderingNode(
1166
+ zodType: z.ZodTypeAny,
1167
+ baseKey: string = "root"
1168
+ ): RenderingNode {
1169
+ const def = {
1170
+ name: baseKey,
1171
+ label: camelize(baseKey, false),
1172
+ zodType,
1173
+ };
1174
+ if (zodType instanceof z.ZodObject) {
1175
+ const columnKeys = Object.keys(zodType.shape);
1176
+ const children = columnKeys.map((key) => {
1177
+ const innerType = zodType.shape[key];
1178
+ return this.zodTypeToRenderingNode(innerType, key);
1179
+ });
1180
+ return {
1181
+ ...def,
1182
+ renderType: "object",
1183
+ children,
1184
+ };
1185
+ } else if (zodType instanceof z.ZodArray) {
1186
+ const innerType = zodType._def.type;
1187
+ if (innerType instanceof z.ZodString && baseKey.includes("images")) {
1188
+ return {
1189
+ ...def,
1190
+ renderType: "array-images",
1191
+ };
1192
+ }
1193
+ return {
1194
+ ...def,
1195
+ renderType: "array",
1196
+ element: this.zodTypeToRenderingNode(innerType, baseKey),
1197
+ };
1198
+ } else if (zodType instanceof z.ZodUnion) {
1199
+ const optionNodes = zodType._def.options.map((opt: z.ZodTypeAny) =>
1200
+ this.zodTypeToRenderingNode(opt, baseKey)
1201
+ );
1202
+ // TODO: ZodUnion이 들어있는 경우 핸들링
1203
+ return optionNodes[0];
1204
+ } else if (zodType instanceof z.ZodOptional) {
1205
+ return {
1206
+ ...this.zodTypeToRenderingNode(zodType._def.innerType, baseKey),
1207
+ optional: true,
1208
+ };
1209
+ } else if (zodType instanceof z.ZodNullable) {
1210
+ return {
1211
+ ...this.zodTypeToRenderingNode(zodType._def.innerType, baseKey),
1212
+ nullable: true,
1213
+ };
1214
+ } else {
1215
+ return {
1216
+ ...def,
1217
+ renderType: this.resolveRenderType(baseKey, zodType),
1218
+ };
1219
+ }
1220
+ }
1221
+
1222
+ async getColumnsNode(smdId: string, subsetKey: string) {
1223
+ const smd = await SMDManager.get(smdId);
1224
+ const subsetA = smd.subsets[subsetKey];
1225
+ if (subsetA === undefined) {
1226
+ throw new ServiceUnavailableException("SubsetA 가 없습니다.");
1227
+ }
1228
+ const propNodes = smd.fieldExprsToPropNodes(subsetA);
1229
+ const rootPropNode: SMDPropNode = {
1230
+ nodeType: "object",
1231
+ children: propNodes,
1232
+ };
1233
+
1234
+ const columnsZodType = (await this.propNodeToZodType(
1235
+ rootPropNode
1236
+ )) as z.ZodObject<any>;
1237
+
1238
+ const columnsNode = this.zodTypeToRenderingNode(columnsZodType);
1239
+ columnsNode.children = columnsNode.children!.map((child) => {
1240
+ if (child.renderType === "object") {
1241
+ const pickedCol = child.children!.find((cc) =>
1242
+ ["title", "name"].includes(cc.name)
1243
+ );
1244
+ if (pickedCol) {
1245
+ return {
1246
+ ...child,
1247
+ renderType: "object-pick",
1248
+ config: {
1249
+ picked: pickedCol.name,
1250
+ },
1251
+ };
1252
+ } else {
1253
+ return child;
1254
+ }
1255
+ } else if (
1256
+ child.renderType === "array" &&
1257
+ child.element &&
1258
+ child.element.renderType === "object"
1259
+ ) {
1260
+ const pickedCol = child.element!.children!.find((cc) =>
1261
+ ["title", "name"].includes(cc.name)
1262
+ );
1263
+ if (pickedCol) {
1264
+ return {
1265
+ ...child,
1266
+ element: {
1267
+ ...child.element,
1268
+ renderType: "object-pick",
1269
+ config: {
1270
+ picked: pickedCol.name,
1271
+ },
1272
+ },
1273
+ };
1274
+ } else {
1275
+ return child;
1276
+ }
1277
+ }
1278
+ return child;
1279
+ });
1280
+
1281
+ return columnsNode;
1282
+ }
1283
+ }