sonamu 0.1.0 → 0.1.4

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 (43) 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 +9 -0
  8. package/dist/entity/entity.d.ts.map +1 -1
  9. package/dist/entity/entity.js +147 -3
  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/view_form.template.d.ts +1 -1
  26. package/dist/templates/view_list.template.d.ts +1 -1
  27. package/dist/types/types.d.ts +5 -5
  28. package/dist/types/types.d.ts.map +1 -1
  29. package/dist/types/types.js.map +1 -1
  30. package/dist/utils/utils.d.ts +1 -1
  31. package/dist/utils/utils.d.ts.map +1 -1
  32. package/dist/utils/utils.js +4 -1
  33. package/dist/utils/utils.js.map +1 -1
  34. package/package.json +1 -1
  35. package/src/entity/entity-manager.ts +1 -0
  36. package/src/entity/entity-utils.ts +2 -0
  37. package/src/entity/entity.ts +180 -3
  38. package/src/entity/migrator.ts +54 -13
  39. package/src/syncer/syncer.ts +84 -44
  40. package/src/templates/entity.template.ts +42 -9
  41. package/src/templates/generated.template.ts +27 -2
  42. package/src/types/types.ts +4 -3
  43. package/src/utils/utils.ts +5 -1
@@ -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(".")};`;
@@ -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: {
@@ -1296,12 +1334,14 @@ export class Syncer {
1296
1334
  await EntityManager.reload();
1297
1335
 
1298
1336
  // generate schemas
1299
- await this.actionGenerateSchemas([entityId]);
1337
+ await this.actionGenerateSchemas([parentId ?? entityId]);
1300
1338
 
1301
1339
  // generate types
1302
- await this.generateTemplate("init_types", {
1303
- entityId,
1304
- });
1340
+ if (parentId === undefined) {
1341
+ await this.generateTemplate("init_types", {
1342
+ entityId,
1343
+ });
1344
+ }
1305
1345
  }
1306
1346
 
1307
1347
  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: [],
@@ -9,7 +9,7 @@ import {
9
9
  import { EntityManager, EntityNamesRecord } from "../entity/entity-manager";
10
10
  import { Entity } from "../entity/entity";
11
11
  import { EntityPropNode, SubsetQuery } from "../types/types";
12
- import { propNodeToZodTypeDef } from "../api/code-converters";
12
+ import { propNodeToZodTypeDef, zodTypeToZodCode } from "../api/code-converters";
13
13
  import { Template } from "./base-template";
14
14
 
15
15
  export class Template__generated extends Template {
@@ -33,7 +33,7 @@ export class Template__generated extends Template {
33
33
  ...(entity.parentId === undefined
34
34
  ? [
35
35
  this.getBaseListParamsTypeSource(entity),
36
- this.getSubsetTypeSource(entity),
36
+ this.getSubsetTypeSource(entity)!,
37
37
  ]
38
38
  : []),
39
39
  ].reduce(
@@ -52,6 +52,31 @@ export class Template__generated extends Template {
52
52
  }
53
53
  );
54
54
 
55
+ // .types.ts의 타입을 참조하는 경우 순환참조(상호참조)가 발생하므로 해당 타입을 가져와 인라인 처리
56
+ const entityTypeKeys = Object.keys(entity.types);
57
+ const cdImportKeys = uniq(typeSource.importKeys).filter((importKey) =>
58
+ entityTypeKeys.includes(importKey)
59
+ );
60
+ if (cdImportKeys.length > 0) {
61
+ typeSource.lines = [
62
+ ...cdImportKeys
63
+ .map((importKey) => [
64
+ `// Imported CustomScalar: ${importKey}`,
65
+ `const ${importKey} = ${zodTypeToZodCode(
66
+ entity.types[importKey]
67
+ )};`,
68
+ `type ${importKey} = z.infer<typeof ${importKey}>`,
69
+ "",
70
+ ])
71
+ .flat(),
72
+ "",
73
+ ...typeSource.lines,
74
+ ];
75
+ typeSource.importKeys = typeSource.importKeys.filter(
76
+ (importKey) => !cdImportKeys.includes(importKey)
77
+ );
78
+ }
79
+
55
80
  // targetAndPath
56
81
  const names = EntityManager.getNamesFromId(entityId);
57
82
  const targetAndPath = this.getTargetAndPath(names);
@@ -41,7 +41,7 @@ export type CommonProp = {
41
41
  nullable?: boolean;
42
42
  toFilter?: true;
43
43
  desc?: string;
44
- dbDefault?: string | number | { raw: string };
44
+ dbDefault?: string;
45
45
  };
46
46
  export type IntegerProp = CommonProp & {
47
47
  type: "integer";
@@ -73,6 +73,8 @@ export type FloatProp = CommonProp & {
73
73
  export type DoubleProp = CommonProp & {
74
74
  type: "double";
75
75
  unsigned?: true;
76
+ precision: number;
77
+ scale: number;
76
78
  };
77
79
  export type DecimalProp = CommonProp & {
78
80
  type: "decimal";
@@ -113,7 +115,6 @@ export type RelationType =
113
115
  | "ManyToMany"
114
116
  | "OneToOne";
115
117
  export type RelationOn =
116
- | "UPDATE"
117
118
  | "CASCADE"
118
119
  | "SET NULL"
119
120
  | "NO ACTION"
@@ -394,7 +395,7 @@ export type MigrationColumn = {
394
395
  nullable: boolean;
395
396
  unsigned?: boolean;
396
397
  length?: number;
397
- defaultTo?: string | number;
398
+ defaultTo?: string;
398
399
  precision?: number;
399
400
  scale?: number;
400
401
  };
@@ -14,11 +14,15 @@ export function globAsync(pathPattern: string): Promise<string[]> {
14
14
  });
15
15
  }
16
16
  export async function importMultiple(
17
- filePaths: string[]
17
+ filePaths: string[],
18
+ doRefresh: boolean = false
18
19
  ): Promise<{ filePath: string; imported: any }[]> {
19
20
  return Promise.all(
20
21
  filePaths.map(async (filePath) => {
21
22
  const importPath = "./" + path.relative(__dirname, filePath);
23
+ if (doRefresh) {
24
+ delete require.cache[require.resolve(importPath)];
25
+ }
22
26
  const imported = await import(importPath);
23
27
  return {
24
28
  filePath,