sonamu 0.1.2 → 0.1.5

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 (67) hide show
  1. package/dist/entity/entity-manager.d.ts.map +1 -1
  2. package/dist/entity/entity-manager.js +1 -0
  3. package/dist/entity/entity-manager.js.map +1 -1
  4. package/dist/entity/entity-utils.d.ts.map +1 -1
  5. package/dist/entity/entity-utils.js +1 -1
  6. package/dist/entity/entity-utils.js.map +1 -1
  7. package/dist/entity/entity.d.ts +4 -0
  8. package/dist/entity/entity.d.ts.map +1 -1
  9. package/dist/entity/entity.js +74 -22
  10. package/dist/entity/entity.js.map +1 -1
  11. package/dist/entity/migrator.d.ts.map +1 -1
  12. package/dist/entity/migrator.js +43 -12
  13. package/dist/entity/migrator.js.map +1 -1
  14. package/dist/syncer/syncer.d.ts +6 -1
  15. package/dist/syncer/syncer.d.ts.map +1 -1
  16. package/dist/syncer/syncer.js +62 -41
  17. package/dist/syncer/syncer.js.map +1 -1
  18. package/dist/templates/entity.template.d.ts +1 -1
  19. package/dist/templates/entity.template.d.ts.map +1 -1
  20. package/dist/templates/entity.template.js +43 -13
  21. package/dist/templates/entity.template.js.map +1 -1
  22. package/dist/templates/generated.template.d.ts.map +1 -1
  23. package/dist/templates/generated.template.js +18 -0
  24. package/dist/templates/generated.template.js.map +1 -1
  25. package/dist/templates/service.template.d.ts.map +1 -1
  26. package/dist/templates/service.template.js +4 -4
  27. package/dist/templates/service.template.js.map +1 -1
  28. package/dist/templates/view_enums_dropdown.template.d.ts +2 -2
  29. package/dist/templates/view_enums_dropdown.template.d.ts.map +1 -1
  30. package/dist/templates/view_enums_dropdown.template.js +14 -13
  31. package/dist/templates/view_enums_dropdown.template.js.map +1 -1
  32. package/dist/templates/view_enums_select.template.d.ts +1 -1
  33. package/dist/templates/view_enums_select.template.d.ts.map +1 -1
  34. package/dist/templates/view_enums_select.template.js +4 -4
  35. package/dist/templates/view_enums_select.template.js.map +1 -1
  36. package/dist/templates/view_form.template.d.ts +2 -5
  37. package/dist/templates/view_form.template.d.ts.map +1 -1
  38. package/dist/templates/view_form.template.js +14 -8
  39. package/dist/templates/view_form.template.js.map +1 -1
  40. package/dist/templates/view_list.template.d.ts +2 -5
  41. package/dist/templates/view_list.template.d.ts.map +1 -1
  42. package/dist/templates/view_list.template.js +17 -20
  43. package/dist/templates/view_list.template.js.map +1 -1
  44. package/dist/types/types.d.ts +15 -30
  45. package/dist/types/types.d.ts.map +1 -1
  46. package/dist/types/types.js +0 -3
  47. package/dist/types/types.js.map +1 -1
  48. package/dist/utils/utils.d.ts +1 -1
  49. package/dist/utils/utils.d.ts.map +1 -1
  50. package/dist/utils/utils.js +4 -1
  51. package/dist/utils/utils.js.map +1 -1
  52. package/package.json +1 -1
  53. package/src/entity/entity-manager.ts +1 -0
  54. package/src/entity/entity-utils.ts +2 -0
  55. package/src/entity/entity.ts +101 -32
  56. package/src/entity/migrator.ts +54 -13
  57. package/src/shared/web.shared.ts.txt +21 -9
  58. package/src/syncer/syncer.ts +88 -45
  59. package/src/templates/entity.template.ts +42 -9
  60. package/src/templates/generated.template.ts +27 -2
  61. package/src/templates/service.template.ts +6 -4
  62. package/src/templates/view_enums_dropdown.template.ts +12 -17
  63. package/src/templates/view_enums_select.template.ts +4 -8
  64. package/src/templates/view_form.template.ts +16 -10
  65. package/src/templates/view_list.template.ts +16 -23
  66. package/src/types/types.ts +4 -6
  67. package/src/utils/utils.ts +5 -1
@@ -24,6 +24,7 @@ import { existsSync, writeFileSync } from "fs";
24
24
  import { z } from "zod";
25
25
  import { Sonamu } from "../api/sonamu";
26
26
  import prettier from "prettier";
27
+ import { nonNullable } from "../utils/utils";
27
28
 
28
29
  export class Entity {
29
30
  id: string;
@@ -414,9 +415,10 @@ export class Entity {
414
415
  };
415
416
  }
416
417
 
417
- const prop = entity.propsDict[propName];
418
+ const prop = entity.props.find((p) => p.name === propName);
418
419
  if (prop === undefined) {
419
- throw new Error(`${this.id} -- 잘못된 FieldExpr ${propName}`);
420
+ console.log({ propName, groups });
421
+ throw new Error(`${entity.id} -- 잘못된 FieldExpr ${propName}`);
420
422
  }
421
423
  return {
422
424
  nodeType: "plain" as const,
@@ -501,7 +503,21 @@ export class Entity {
501
503
  }
502
504
 
503
505
  getTableColumns(): string[] {
504
- return this.props.map((prop) => prop.name);
506
+ return this.props
507
+ .map((prop) => {
508
+ if (prop.type === "relation") {
509
+ if (
510
+ prop.relationType === "BelongsToOne" ||
511
+ (prop.relationType === "OneToOne" && prop.hasJoinColumn === true)
512
+ ) {
513
+ return `${prop.name}_id`;
514
+ } else {
515
+ return null;
516
+ }
517
+ }
518
+ return prop.name;
519
+ })
520
+ .filter(nonNullable);
505
521
  }
506
522
 
507
523
  registerModulePaths() {
@@ -673,10 +689,40 @@ export class Entity {
673
689
  await this.save();
674
690
  }
675
691
 
692
+ analyzeSubsetField(subsetField: string): {
693
+ entityId: string;
694
+ propName: string;
695
+ }[] {
696
+ const arr = subsetField.split(".");
697
+
698
+ let entityId = this.id;
699
+ const result: {
700
+ entityId: string;
701
+ propName: string;
702
+ }[] = [];
703
+ for (let i = 0; i < arr.length; i++) {
704
+ const propName = arr[i];
705
+ result.push({
706
+ entityId,
707
+ propName,
708
+ });
709
+
710
+ const prop = EntityManager.get(entityId).props.find(
711
+ (p) => p.name === propName
712
+ );
713
+ if (!prop) {
714
+ throw new Error(`${entityId}의 잘못된 서브셋키 ${subsetField}`);
715
+ }
716
+ if (isRelationProp(prop)) {
717
+ entityId = prop.with;
718
+ }
719
+ }
720
+ return result;
721
+ }
722
+
676
723
  async modifyProp(newProp: EntityProp, at: number): Promise<void> {
677
- // 프롭 수정
724
+ // 이전 프롭 이름 저장
678
725
  const oldName = this.props[at].name;
679
- this.props[at] = newProp;
680
726
 
681
727
  // 저장할 엔티티
682
728
  const entities: Entity[] = [this];
@@ -690,30 +736,39 @@ export class Entity {
690
736
  const relEntitySubsetKeys = Object.keys(relEntity.subsets);
691
737
  for (const subsetKey of relEntitySubsetKeys) {
692
738
  const subset = relEntity.subsets[subsetKey];
693
- const oldSubsetFields = subset.filter(
694
- (field) =>
695
- field.endsWith(oldName) &&
696
- relEntity.getEntityIdFromSubsetField(field) === this.id
697
- );
698
- if (oldSubsetFields.length > 0) {
699
- relEntity.subsets[subsetKey] = relEntity.subsets[subsetKey].map(
700
- (oldField) =>
701
- oldSubsetFields.includes(oldField)
702
- ? oldField.replace(`${oldName}`, `${newProp.name}`)
703
- : oldField
739
+
740
+ // 서브셋 필드를 순회하며, 엔티티-프롭 단위로 분석한 후 현재 엔티티-프롭과 일치하는 경우 수정 처리
741
+ const modifiedSubsetFields = subset.map((subsetField) => {
742
+ const analyzed = relEntity.analyzeSubsetField(subsetField);
743
+ const modified = analyzed.map((a) =>
744
+ a.propName === oldName && a.entityId === this.id
745
+ ? {
746
+ ...a,
747
+ propName: newProp.name,
748
+ }
749
+ : a
704
750
  );
751
+ // 분석한 필드를 다시 서브셋 필드로 복구
752
+ return modified.map((a) => a.propName).join(".");
753
+ });
754
+
755
+ if (subset.join(",") !== modifiedSubsetFields.join(",")) {
756
+ relEntity.subsets[subsetKey] = modifiedSubsetFields;
705
757
  entities.push(relEntity);
706
758
  }
707
759
  }
708
760
  }
709
761
  }
710
762
 
763
+ // 프롭 수정
764
+ this.props[at] = newProp;
765
+
711
766
  await Promise.all(entities.map(async (entity) => entity.save()));
712
767
  }
713
768
 
714
769
  async delProp(at: number): Promise<void> {
770
+ // 이전 프롭 이름 저장
715
771
  const oldName = this.props[at].name;
716
- this.props.splice(at, 1);
717
772
 
718
773
  // 저장할 엔티티
719
774
  const entities: Entity[] = [this];
@@ -725,20 +780,32 @@ export class Entity {
725
780
  const relEntitySubsetKeys = Object.keys(relEntity.subsets);
726
781
  for (const subsetKey of relEntitySubsetKeys) {
727
782
  const subset = relEntity.subsets[subsetKey];
728
- const oldSubsetFields = subset.filter(
729
- (field) =>
730
- field.endsWith(oldName) &&
731
- relEntity.getEntityIdFromSubsetField(field) === this.id
732
- );
733
- if (oldSubsetFields.length > 0) {
734
- relEntity.subsets[subsetKey] = relEntity.subsets[subsetKey].filter(
735
- (oldField) => oldSubsetFields.includes(oldField) === false
736
- );
783
+ // 서브셋 필드를 순회하며, 엔티티-프롭 단위로 분석한 후 현재 엔티티-프롭과 일치하는 경우 이후의 필드를 제외
784
+ const modifiedSubsetFields = subset
785
+ .map((subsetField) => {
786
+ const analyzed = relEntity.analyzeSubsetField(subsetField);
787
+ if (
788
+ analyzed.find(
789
+ (a) => a.propName === oldName && a.entityId === this.id
790
+ )
791
+ ) {
792
+ return null;
793
+ } else {
794
+ return subsetField;
795
+ }
796
+ })
797
+ .filter(nonNullable);
798
+
799
+ if (subset.join(",") !== modifiedSubsetFields.join(",")) {
800
+ relEntity.subsets[subsetKey] = modifiedSubsetFields;
737
801
  entities.push(relEntity);
738
802
  }
739
803
  }
740
804
  }
741
805
 
806
+ // 프롭 삭제
807
+ this.props.splice(at, 1);
808
+
742
809
  await Promise.all(entities.map(async (entity) => entity.save()));
743
810
  }
744
811
 
@@ -751,15 +818,17 @@ export class Entity {
751
818
  const arr = subsetField.split(".").slice(0, -1);
752
819
 
753
820
  // 서브셋 필드를 내려가면서 마지막으로 relation된 엔티티를 찾음
754
- const lastEntity = arr.reduce((entity, field) => {
755
- const relProp = entity.props.find((p) => p.name === field);
821
+ const lastEntityId = arr.reduce((entityId, field) => {
822
+ const relProp = EntityManager.get(entityId).props.find(
823
+ (p) => p.name === field
824
+ );
756
825
  if (!relProp || relProp.type !== "relation") {
757
- console.debug({ arr, thisId: this.id });
826
+ console.debug({ arr, thisId: this.id, entityId, field });
758
827
  throw new Error(`잘못된 서브셋키 ${subsetField}`);
759
828
  }
760
- return EntityManager.get(relProp.with);
761
- }, this as Entity);
762
- return lastEntity.id;
829
+ return relProp.with;
830
+ }, this.id);
831
+ return lastEntityId;
763
832
  }
764
833
 
765
834
  async moveProp(at: number, to: number): Promise<void> {
@@ -137,6 +137,17 @@ export class Migrator {
137
137
  }> {
138
138
  const srcMigrationsDir = `${Sonamu.apiRootPath}/src/migrations`;
139
139
  const distMigrationsDir = `${Sonamu.apiRootPath}/dist/migrations`;
140
+
141
+ if (existsSync(srcMigrationsDir) === false) {
142
+ mkdirSync(srcMigrationsDir, {
143
+ recursive: true,
144
+ });
145
+ }
146
+ if (existsSync(distMigrationsDir) === false) {
147
+ mkdirSync(distMigrationsDir, {
148
+ recursive: true,
149
+ });
150
+ }
140
151
  const srcMigrations = readdirSync(srcMigrationsDir)
141
152
  .filter((f) => f.endsWith(".ts"))
142
153
  .map((f) => f.split(".")[0]);
@@ -203,9 +214,10 @@ export class Migrator {
203
214
  connKeys.map(async (connKey) => {
204
215
  const knexOptions = Sonamu.dbConfig[connKey];
205
216
  const tConn = knex(knexOptions);
217
+
206
218
  const status = await (async () => {
207
219
  try {
208
- return tConn.migrate.status();
220
+ return await tConn.migrate.status();
209
221
  } catch (err) {
210
222
  return "error";
211
223
  }
@@ -874,12 +886,14 @@ export class Migrator {
874
886
  name: dbColumn.Field,
875
887
  nullable: dbColumn.Null !== "NO",
876
888
  ...dbColType,
877
- ...propIf(dbColumn.Default !== null, {
878
- defaultTo:
879
- dbColType.type === "float"
880
- ? parseFloat(dbColumn.Default ?? "0").toString()
881
- : dbColumn.Default,
882
- }),
889
+ ...(() => {
890
+ if (dbColumn.Default !== null) {
891
+ return {
892
+ defaultTo: dbColumn.Default,
893
+ };
894
+ }
895
+ return {};
896
+ })(),
883
897
  };
884
898
  });
885
899
 
@@ -1020,7 +1034,23 @@ export class Migrator {
1020
1034
  ): Promise<[DBColumn[], DBIndex[], DBForeign[]]> {
1021
1035
  // 테이블 정보
1022
1036
  try {
1023
- const [cols] = await compareDB.raw(`SHOW FIELDS FROM ${tableName}`);
1037
+ const [_cols] = (await compareDB.raw(
1038
+ `SHOW FIELDS FROM ${tableName}`
1039
+ )) as [DBColumn[]];
1040
+ const cols = _cols.map((col) => ({
1041
+ ...col,
1042
+ // Default 값은 숫자나 MySQL Expression이 아닌 경우 ""로 감싸줌
1043
+ ...(col.Default !== null
1044
+ ? {
1045
+ Default:
1046
+ col.Default.replace(/[0-9]+/g, "").length > 0 &&
1047
+ col.Extra !== "DEFAULT_GENERATED"
1048
+ ? `"${col.Default}"`
1049
+ : col.Default,
1050
+ }
1051
+ : {}),
1052
+ }));
1053
+
1024
1054
  const [indexes] = await compareDB.raw(`SHOW INDEX FROM ${tableName}`);
1025
1055
  const [[row]] = await compareDB.raw(`SHOW CREATE TABLE ${tableName}`);
1026
1056
  const ddl = row["Create Table"];
@@ -1115,10 +1145,14 @@ export class Migrator {
1115
1145
  ? { length: prop.length }
1116
1146
  : {}),
1117
1147
  nullable: prop.nullable === true,
1118
- // DB에선 무조건 string으로 리턴되므로 반드시 string 처리
1119
- ...propIf(prop.dbDefault !== undefined, {
1120
- defaultTo: "" + prop.dbDefault,
1121
- }),
1148
+ ...(() => {
1149
+ if (prop.dbDefault !== undefined) {
1150
+ return {
1151
+ defaultTo: prop.dbDefault,
1152
+ };
1153
+ }
1154
+ return {};
1155
+ })(),
1122
1156
  // Decimal, Float 타입의 경우 precision, scale 추가
1123
1157
  ...(isDecimalProp(prop) || isFloatProp(prop)
1124
1158
  ? {
@@ -1286,7 +1320,14 @@ export class Migrator {
1286
1320
 
1287
1321
  // defaultTo
1288
1322
  if (column.defaultTo !== undefined) {
1289
- chains.push(`defaultTo(knex.raw('${column.defaultTo}'))`);
1323
+ if (
1324
+ typeof column.defaultTo === "string" &&
1325
+ column.defaultTo.startsWith(`"`)
1326
+ ) {
1327
+ chains.push(`defaultTo(${column.defaultTo})`);
1328
+ } else {
1329
+ chains.push(`defaultTo(knex.raw('${column.defaultTo}'))`);
1330
+ }
1290
1331
  }
1291
1332
 
1292
1333
  return `table.${chains.join(".")};`;
@@ -4,6 +4,7 @@
4
4
  import type { AxiosRequestConfig } from "axios";
5
5
  import axios from "axios";
6
6
  import { z, ZodIssue } from "zod";
7
+ import qs from 'qs';
7
8
 
8
9
  export async function fetch(options: AxiosRequestConfig) {
9
10
  try {
@@ -71,12 +72,23 @@ export type SWRError = {
71
72
  message: string;
72
73
  statusCode: number;
73
74
  };
74
- export async function swrFetcher(
75
- url: string,
76
- params: string = ""
77
- ): Promise<any> {
75
+ export async function swrFetcher(args: [string, object]): Promise<any> {
78
76
  try {
79
- const res = await axios.get(url + "?" + params);
77
+ const [url, params] = args;
78
+ const res = await axios.get(`${url}?${qs.stringify(params)}`);
79
+ return res.data;
80
+ } catch (e: any) {
81
+ const error: any = new Error(
82
+ e.response.data.message ?? e.response.message ?? "Unknown"
83
+ );
84
+ error.statusCode = e.response?.data.statusCode ?? e.response.status;
85
+ throw error;
86
+ }
87
+ }
88
+ export async function swrPostFetcher(args: [string, object]): Promise<any> {
89
+ try {
90
+ const [url, params] = args;
91
+ const res = await axios.post(url, params);
80
92
  return res.data;
81
93
  } catch (e: any) {
82
94
  const error: any = new Error(
@@ -87,13 +99,13 @@ export async function swrFetcher(
87
99
  }
88
100
  }
89
101
  export function handleConditional(
90
- route: string | string[],
102
+ args: [string, object],
91
103
  conditional?: () => boolean
92
- ): string | string[] | null {
104
+ ): [string, object] | null {
93
105
  if (conditional) {
94
- return conditional() ? route : null;
106
+ return conditional() ? args : null;
95
107
  }
96
- return route;
108
+ return args;
97
109
  }
98
110
 
99
111
  /*
@@ -159,7 +159,7 @@ export class Syncer {
159
159
  );
160
160
 
161
161
  // 현재 checksums
162
- const currentChecksums = await this.getCurrentChecksums();
162
+ let currentChecksums = await this.getCurrentChecksums();
163
163
  // 이전 checksums
164
164
  const previousChecksums = await this.getPreviousChecksums();
165
165
 
@@ -190,12 +190,26 @@ export class Syncer {
190
190
  // 변경된 파일들을 타입별로 분리하여 각 타입별 액션 처리
191
191
  const diffTypes = Object.keys(diffGroups);
192
192
 
193
- // 트리거: entity
193
+ // 트리거: entity, types
194
194
  // 액션: 스키마 생성
195
- if (diffTypes.includes("entity")) {
195
+ if (diffTypes.includes("entity") || diffTypes.includes("types")) {
196
196
  console.log("// 액션: 스키마 생성");
197
- const entityIds = this.getEntityIdFromPath(diffGroups["entity"]);
197
+ const entityIds = this.getEntityIdFromPath([
198
+ ...(diffGroups["entity"] ?? []),
199
+ ...(diffGroups["types"] ?? []),
200
+ ]);
198
201
  await this.actionGenerateSchemas(entityIds);
202
+
203
+ // 타입이 변경된 경우 generated 싱크까지 동시에 처리 후 체크섬 갱신
204
+ if (diffTypes.includes("types")) {
205
+ diffGroups["generated"] = uniq([
206
+ ...(diffGroups["generated"] ?? []),
207
+ ...diffGroups["types"].map((p) =>
208
+ p.replace(".types.ts", ".generated.ts")
209
+ ),
210
+ ]);
211
+ currentChecksums = await this.getCurrentChecksums();
212
+ }
199
213
  }
200
214
 
201
215
  // 트리거: types, enums, generated 변경시
@@ -207,11 +221,13 @@ export class Syncer {
207
221
  ) {
208
222
  console.log("// 액션: 파일 싱크 types / functions / generated");
209
223
 
210
- const tsPaths = [
211
- ...(diffGroups["types"] ?? []),
212
- ...(diffGroups["functions"] ?? []),
213
- ...(diffGroups["generated"] ?? []),
214
- ].map((p) => p.replace("/dist/", "/src/").replace(".js", ".ts"));
224
+ const tsPaths = uniq(
225
+ [
226
+ ...(diffGroups["types"] ?? []),
227
+ ...(diffGroups["functions"] ?? []),
228
+ ...(diffGroups["generated"] ?? []),
229
+ ].map((p) => p.replace("/dist/", "/src/").replace(".js", ".ts"))
230
+ );
215
231
  await this.actionSyncFilesToTargets(tsPaths);
216
232
  }
217
233
 
@@ -229,10 +245,12 @@ export class Syncer {
229
245
  }
230
246
 
231
247
  getEntityIdFromPath(filePaths: string[]): string[] {
232
- return filePaths.map((p) => {
233
- const matched = p.match(/application\/(.+)\//);
234
- return camelize(matched![1].replace(/\-/g, "_"));
235
- });
248
+ return uniq(
249
+ filePaths.map((p) => {
250
+ const matched = p.match(/application\/(.+)\//);
251
+ return camelize(matched![1].replace(/\-/g, "_"));
252
+ })
253
+ );
236
254
  }
237
255
 
238
256
  async actionGenerateSchemas(entityIds: string[]): Promise<string[]> {
@@ -702,8 +720,10 @@ export class Syncer {
702
720
  return this.models;
703
721
  }
704
722
 
705
- async autoloadTypes(): Promise<{ [typeName: string]: z.ZodObject<any> }> {
706
- if (Object.keys(this.types).length > 0) {
723
+ async autoloadTypes(
724
+ doRefresh: boolean = false
725
+ ): Promise<{ [typeName: string]: z.ZodObject<any> }> {
726
+ if (!doRefresh && Object.keys(this.types).length > 0) {
707
727
  return this.types;
708
728
  }
709
729
 
@@ -716,7 +736,7 @@ export class Syncer {
716
736
  const filePaths = (
717
737
  await Promise.all(pathPatterns.map((pattern) => globAsync(pattern)))
718
738
  ).flat();
719
- const modules = await importMultiple(filePaths);
739
+ const modules = await importMultiple(filePaths, doRefresh);
720
740
  const functions = modules
721
741
  .map(({ imported }) => Object.entries(imported))
722
742
  .flat();
@@ -917,10 +937,7 @@ export class Syncer {
917
937
  };
918
938
 
919
939
  // 키 children
920
- let keys: TemplateKey[] = [key];
921
- // if (key === "entity") {
922
- // keys = ["entity", "init_generated", "init_types"];
923
- // }
940
+ const keys: TemplateKey[] = [key];
924
941
 
925
942
  // 템플릿 렌더
926
943
  const pathAndCodes = (
@@ -940,27 +957,30 @@ export class Syncer {
940
957
  - 옵션3 : 메인 파일만 그대로 두고, 파생 파일은 전부 생성함 => 이게 맞지 않나?
941
958
  */
942
959
 
943
- let filteredPathAndCodes: PathAndCode[] = [];
944
- if (generateOptions.overwrite === true) {
945
- filteredPathAndCodes = pathAndCodes;
946
- } else {
947
- filteredPathAndCodes = pathAndCodes.filter((pathAndCode, index) => {
948
- if (index === 0) {
949
- const { targets } = Sonamu.config.sync;
950
- const filePath = `${Sonamu.appRootPath}/${pathAndCode.path}`;
951
- const dstFilePaths = targets.map((target) =>
952
- filePath.replace("/:target/", `/${target}/`)
953
- );
954
- return dstFilePaths.every((dstPath) => existsSync(dstPath) === false);
955
- } else {
956
- return true;
957
- }
958
- });
959
- if (filteredPathAndCodes.length === 0) {
960
- throw new AlreadyProcessedException(
961
- "이미 경로에 모든 파일이 존재합니다."
962
- );
960
+ const filteredPathAndCodes: PathAndCode[] = (() => {
961
+ if (generateOptions.overwrite === true) {
962
+ return pathAndCodes;
963
+ } else {
964
+ return pathAndCodes.filter((pathAndCode, index) => {
965
+ if (index === 0) {
966
+ const { targets } = Sonamu.config.sync;
967
+ const filePath = `${Sonamu.appRootPath}/${pathAndCode.path}`;
968
+ const dstFilePaths = targets.map((target) =>
969
+ filePath.replace("/:target/", `/${target}/`)
970
+ );
971
+ return dstFilePaths.every(
972
+ (dstPath) => existsSync(dstPath) === false
973
+ );
974
+ } else {
975
+ return true;
976
+ }
977
+ });
963
978
  }
979
+ })();
980
+ if (filteredPathAndCodes.length === 0) {
981
+ throw new AlreadyProcessedException(
982
+ "이미 경로에 모든 파일이 존재합니다."
983
+ );
964
984
  }
965
985
 
966
986
  return Promise.all(
@@ -970,6 +990,24 @@ export class Syncer {
970
990
  );
971
991
  }
972
992
 
993
+ checkExistsGenCode(
994
+ entityId: string,
995
+ templateKey: TemplateKey,
996
+ enumId?: string
997
+ ): { subPath: string; fullPath: string; isExists: boolean } {
998
+ const { target, path: genPath } = this.getTemplate(
999
+ templateKey
1000
+ ).getTargetAndPath(EntityManager.getNamesFromId(entityId), enumId);
1001
+
1002
+ const fullPath = path.join(Sonamu.appRootPath, target, genPath);
1003
+ const subPath = path.join(target, genPath);
1004
+ return {
1005
+ subPath,
1006
+ fullPath,
1007
+ isExists: existsSync(fullPath),
1008
+ };
1009
+ }
1010
+
973
1011
  checkExists(
974
1012
  entityId: string,
975
1013
  enums: {
@@ -1217,7 +1255,10 @@ export class Syncer {
1217
1255
  }
1218
1256
  }
1219
1257
 
1220
- async getColumnsNode(entityId: string, subsetKey: string) {
1258
+ async getColumnsNode(
1259
+ entityId: string,
1260
+ subsetKey: string
1261
+ ): Promise<RenderingNode> {
1221
1262
  const entity = await EntityManager.get(entityId);
1222
1263
  const subsetA = entity.subsets[subsetKey];
1223
1264
  if (subsetA === undefined) {
@@ -1296,12 +1337,14 @@ export class Syncer {
1296
1337
  await EntityManager.reload();
1297
1338
 
1298
1339
  // generate schemas
1299
- await this.actionGenerateSchemas([entityId]);
1340
+ await this.actionGenerateSchemas([parentId ?? entityId]);
1300
1341
 
1301
1342
  // generate types
1302
- await this.generateTemplate("init_types", {
1303
- entityId,
1304
- });
1343
+ if (parentId === undefined) {
1344
+ await this.generateTemplate("init_types", {
1345
+ entityId,
1346
+ });
1347
+ }
1305
1348
  }
1306
1349
 
1307
1350
  async delEntity(entityId: string): Promise<{ delPaths: string[] }> {
@@ -7,10 +7,10 @@ export class Template__entity extends Template {
7
7
  super("entity");
8
8
  }
9
9
 
10
- getTargetAndPath(names: EntityNamesRecord) {
10
+ getTargetAndPath(names: EntityNamesRecord, parentNames?: EntityNamesRecord) {
11
11
  return {
12
12
  target: "api/src/application",
13
- path: `${names.fs}/${names.fs}.entity.json`,
13
+ path: `${(parentNames ?? names).fs}/${names.fs}.entity.json`,
14
14
  };
15
15
  }
16
16
 
@@ -18,30 +18,63 @@ export class Template__entity extends Template {
18
18
  const { entityId, title, parentId, table } = options;
19
19
  const names = EntityManager.getNamesFromId(entityId);
20
20
 
21
+ const parent = (() => {
22
+ if (parentId) {
23
+ return {
24
+ names: EntityManager.getNamesFromId(parentId),
25
+ entity: EntityManager.get(parentId),
26
+ };
27
+ } else {
28
+ return null;
29
+ }
30
+ })();
31
+
21
32
  return {
22
- ...this.getTargetAndPath(names),
33
+ ...this.getTargetAndPath(names, parent?.names ?? names),
23
34
  body: JSON.stringify({
24
35
  id: entityId,
25
36
  title: title ?? entityId,
26
37
  parentId,
27
38
  table: table ?? names.fsPlural.replace(/\-/g, "_"),
28
39
  props: [
29
- { name: "id", type: "integer", unsigned: true },
40
+ { name: "id", type: "integer", unsigned: true, desc: "ID" },
41
+ ...(parent
42
+ ? [
43
+ {
44
+ type: "relation",
45
+ name: parent.names.camel,
46
+ relationType: "BelongsToOne",
47
+ with: parentId,
48
+ onUpdate: "CASCADE",
49
+ onDelete: "CASCADE",
50
+ desc: parent.entity.title,
51
+ },
52
+ ]
53
+ : []),
30
54
  {
31
55
  name: "created_at",
32
56
  type: "timestamp",
57
+ desc: "등록일시",
33
58
  dbDefault: "CURRENT_TIMESTAMP",
34
59
  },
35
60
  ],
36
61
  indexes: [],
37
62
  subsets: {
38
- A: ["id", "created_at"],
63
+ ...(parentId
64
+ ? {}
65
+ : {
66
+ A: ["id", "created_at"],
67
+ }),
39
68
  },
40
69
  enums: {
41
- [`${names.capital}OrderBy`]: {
42
- "id-desc": "ID최신순",
43
- },
44
- [`${names.capital}SearchField`]: { id: "ID" },
70
+ ...(parentId
71
+ ? {}
72
+ : {
73
+ [`${names.capital}OrderBy`]: {
74
+ "id-desc": "ID최신순",
75
+ },
76
+ [`${names.capital}SearchField`]: { id: "ID" },
77
+ }),
45
78
  },
46
79
  }).trim(),
47
80
  importKeys: [],