sonamu 0.3.1 → 0.4.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 (83) hide show
  1. package/.pnp.cjs +11 -0
  2. package/dist/base-model-BzMJ2E_I.d.mts +43 -0
  3. package/dist/base-model-CWRKUX49.d.ts +43 -0
  4. package/dist/bin/cli.js +118 -89
  5. package/dist/bin/cli.js.map +1 -1
  6. package/dist/bin/cli.mjs +74 -45
  7. package/dist/bin/cli.mjs.map +1 -1
  8. package/dist/chunk-FLPD24HS.mjs +231 -0
  9. package/dist/chunk-FLPD24HS.mjs.map +1 -0
  10. package/dist/chunk-I2MMJRJN.mjs +1550 -0
  11. package/dist/chunk-I2MMJRJN.mjs.map +1 -0
  12. package/dist/{chunk-MPXE4IHO.mjs → chunk-PP2PSSAG.mjs} +5284 -5617
  13. package/dist/chunk-PP2PSSAG.mjs.map +1 -0
  14. package/dist/chunk-QK5XXJUX.mjs +280 -0
  15. package/dist/chunk-QK5XXJUX.mjs.map +1 -0
  16. package/dist/chunk-U636LQJJ.js +231 -0
  17. package/dist/chunk-U636LQJJ.js.map +1 -0
  18. package/dist/chunk-W7KDVJLQ.js +280 -0
  19. package/dist/chunk-W7KDVJLQ.js.map +1 -0
  20. package/dist/{chunk-YXILRRDT.js → chunk-XT6LHCX5.js} +5252 -5585
  21. package/dist/chunk-XT6LHCX5.js.map +1 -0
  22. package/dist/chunk-Z2P7XTXE.js +1550 -0
  23. package/dist/chunk-Z2P7XTXE.js.map +1 -0
  24. package/dist/database/drivers/knex/base-model.d.mts +16 -0
  25. package/dist/database/drivers/knex/base-model.d.ts +16 -0
  26. package/dist/database/drivers/knex/base-model.js +55 -0
  27. package/dist/database/drivers/knex/base-model.js.map +1 -0
  28. package/dist/database/drivers/knex/base-model.mjs +56 -0
  29. package/dist/database/drivers/knex/base-model.mjs.map +1 -0
  30. package/dist/database/drivers/kysely/base-model.d.mts +22 -0
  31. package/dist/database/drivers/kysely/base-model.d.ts +22 -0
  32. package/dist/database/drivers/kysely/base-model.js +64 -0
  33. package/dist/database/drivers/kysely/base-model.js.map +1 -0
  34. package/dist/database/drivers/kysely/base-model.mjs +65 -0
  35. package/dist/database/drivers/kysely/base-model.mjs.map +1 -0
  36. package/dist/index.d.mts +220 -926
  37. package/dist/index.d.ts +220 -926
  38. package/dist/index.js +13 -26
  39. package/dist/index.js.map +1 -1
  40. package/dist/index.mjs +18 -31
  41. package/dist/index.mjs.map +1 -1
  42. package/dist/model-CAH_4oQh.d.mts +1042 -0
  43. package/dist/model-CAH_4oQh.d.ts +1042 -0
  44. package/import-to-require.js +27 -0
  45. package/package.json +23 -2
  46. package/src/api/caster.ts +6 -0
  47. package/src/api/code-converters.ts +3 -1
  48. package/src/api/sonamu.ts +41 -22
  49. package/src/bin/cli.ts +78 -46
  50. package/src/database/_batch_update.ts +16 -11
  51. package/src/database/base-model.abstract.ts +97 -0
  52. package/src/database/base-model.ts +214 -280
  53. package/src/database/code-generator.ts +72 -0
  54. package/src/database/db.abstract.ts +75 -0
  55. package/src/database/db.ts +21 -82
  56. package/src/database/drivers/knex/base-model.ts +55 -0
  57. package/src/database/drivers/knex/client.ts +209 -0
  58. package/src/database/drivers/knex/db.ts +227 -0
  59. package/src/database/drivers/knex/generator.ts +659 -0
  60. package/src/database/drivers/kysely/base-model.ts +89 -0
  61. package/src/database/drivers/kysely/client.ts +309 -0
  62. package/src/database/drivers/kysely/db.ts +238 -0
  63. package/src/database/drivers/kysely/generator.ts +714 -0
  64. package/src/database/types.ts +117 -0
  65. package/src/database/upsert-builder.ts +31 -18
  66. package/src/entity/entity-utils.ts +1 -1
  67. package/src/entity/migrator.ts +98 -693
  68. package/src/index.ts +1 -1
  69. package/src/syncer/syncer.ts +69 -27
  70. package/src/templates/generated_http.template.ts +14 -0
  71. package/src/templates/kysely_types.template.ts +205 -0
  72. package/src/templates/model.template.ts +2 -139
  73. package/src/templates/service.template.ts +3 -1
  74. package/src/testing/_relation-graph.ts +111 -0
  75. package/src/testing/fixture-manager.ts +216 -332
  76. package/src/types/types.ts +56 -6
  77. package/src/utils/utils.ts +56 -4
  78. package/src/utils/zod-error.ts +189 -0
  79. package/tsconfig.json +2 -2
  80. package/tsup.config.js +11 -10
  81. package/dist/chunk-MPXE4IHO.mjs.map +0 -1
  82. package/dist/chunk-YXILRRDT.js.map +0 -1
  83. /package/src/database/{knex-plugins → drivers/knex/plugins}/knex-on-duplicate-update.ts +0 -0
package/src/index.ts CHANGED
@@ -2,9 +2,9 @@ export * from "./api/code-converters";
2
2
  export * from "./api/context";
3
3
  export * from "./api/decorators";
4
4
  export * from "./api/sonamu";
5
- export * from "./database/base-model";
6
5
  export * from "./database/db";
7
6
  export * from "./database/upsert-builder";
7
+ export * from "./database/types";
8
8
  export * from "./exceptions/error-handler";
9
9
  export * from "./exceptions/so-exceptions";
10
10
  export * from "./entity/entity";
@@ -75,6 +75,9 @@ import { Template__generated_http } from "../templates/generated_http.template";
75
75
  import { Sonamu } from "../api/sonamu";
76
76
  import { execSync } from "child_process";
77
77
  import { Template__generated_sso } from "../templates/generated_sso.template";
78
+ import { Template__kysely_interface } from "../templates/kysely_types.template";
79
+ import { DB } from "../database/db";
80
+ import { setTimeout as setTimeoutPromises } from "timers/promises";
78
81
 
79
82
  type FileType = "model" | "types" | "functions" | "generated" | "entity";
80
83
  type GlobPattern = {
@@ -111,6 +114,7 @@ export class Syncer {
111
114
  }[] = [];
112
115
  types: { [typeName: string]: z.ZodObject<any> } = {};
113
116
  models: { [modelName: string]: unknown } = {};
117
+ isSyncing: boolean = false;
114
118
 
115
119
  get checksumsPath(): string {
116
120
  return path.join(Sonamu.apiRootPath, "/.so-checksum");
@@ -167,6 +171,23 @@ export class Syncer {
167
171
  return;
168
172
  }
169
173
 
174
+ const abc = new AbortController();
175
+ this.isSyncing = true;
176
+ const onSIGUSR2 = async () => {
177
+ if (this.isSyncing === false) {
178
+ process.exit(0);
179
+ }
180
+ console.log(chalk.magentaBright(`wait for syncing done....`));
181
+
182
+ // 싱크 완료 대기
183
+ try {
184
+ await setTimeoutPromises(20000, "waiting-sync", { signal: abc.signal });
185
+ } catch {}
186
+ console.log(chalk.magentaBright(`Syncing DONE!`));
187
+ process.exit(0);
188
+ };
189
+ process.on("SIGUSR2", onSIGUSR2);
190
+
170
191
  // 변경된 파일 찾기
171
192
  const diff = _.differenceWith(
172
193
  currentChecksums,
@@ -193,6 +214,18 @@ export class Syncer {
193
214
  console.log("// 액션: 스키마 생성");
194
215
  await this.actionGenerateSchemas();
195
216
 
217
+ if (
218
+ DB.baseConfig?.client === "kysely" &&
219
+ DB.baseConfig.types?.enabled !== false
220
+ ) {
221
+ console.log("// 액션: kysely 인터페이스 생성");
222
+ await this.generateTemplate(
223
+ "kysely_interface",
224
+ {},
225
+ { overwrite: true }
226
+ );
227
+ }
228
+
196
229
  // generated 싱크까지 동시에 처리 후 체크섬 갱신
197
230
  diffGroups["generated"] = _.uniq([
198
231
  ...(diffGroups["generated"] ?? []),
@@ -232,6 +265,11 @@ export class Syncer {
232
265
 
233
266
  // 저장
234
267
  await this.saveChecksums(currentChecksums);
268
+
269
+ // 싱크 종료
270
+ this.isSyncing = false;
271
+ abc.abort();
272
+ process.off("SIGUSR2", onSIGUSR2);
235
273
  }
236
274
 
237
275
  getEntityIdFromPath(filePaths: string[]): string[] {
@@ -275,23 +313,12 @@ export class Syncer {
275
313
  }
276
314
 
277
315
  async actionGenerateHttps(entityIds: string[]): Promise<string[]> {
278
- return (
279
- await Promise.all(
280
- entityIds.map(async (entityId) =>
281
- this.generateTemplate(
282
- "generated_http",
283
- {
284
- entityId,
285
- },
286
- {
287
- overwrite: true,
288
- }
289
- )
290
- )
291
- )
292
- )
293
- .flat()
294
- .flat();
316
+ const [res] = await this.generateTemplate(
317
+ "generated_http",
318
+ { entityId: entityIds[0] },
319
+ { overwrite: true }
320
+ );
321
+ return res;
295
322
  }
296
323
 
297
324
  async copyFileWithReplaceCoreToShared(fromPath: string, toPath: string) {
@@ -661,12 +688,23 @@ export class Syncer {
661
688
  console.debug({ name, type, paramDec });
662
689
  }
663
690
 
664
- return {
691
+ const result: ApiParam = {
665
692
  name: name.escapedText ? name.escapedText.toString() : `nonameAt${index}`,
666
693
  type,
667
694
  optional: paramDec.optional === true,
668
695
  defaultDef: paramDec?.defaultDef,
669
696
  };
697
+
698
+ // 구조분해할당의 경우 타입이름 사용
699
+ if (
700
+ ts.isObjectBindingPattern(name) &&
701
+ ts.isTypeReferenceNode(paramDec.type) &&
702
+ ts.isIdentifier(paramDec.type.typeName)
703
+ ) {
704
+ result.name = inflection.camelize(paramDec.type.typeName.text, true);
705
+ }
706
+
707
+ return result;
670
708
  };
671
709
 
672
710
  printNode(
@@ -787,6 +825,8 @@ export class Syncer {
787
825
  return new Template__view_enums_dropdown();
788
826
  } else if (key === "view_enums_buttonset") {
789
827
  return new Template__view_enums_buttonset();
828
+ } else if (key === "kysely_interface") {
829
+ return new Template__kysely_interface();
790
830
  } else {
791
831
  throw new BadRequestException(`잘못된 템플릿 키 ${key}`);
792
832
  }
@@ -1340,15 +1380,17 @@ export class Syncer {
1340
1380
  // reload entities
1341
1381
  await EntityManager.reload();
1342
1382
 
1343
- // generate schemas
1344
- await this.actionGenerateSchemas();
1345
-
1346
- // generate types
1347
- if (form.parentId === undefined) {
1348
- await this.generateTemplate("init_types", {
1349
- entityId: form.entityId,
1350
- });
1351
- }
1383
+ // generate schemas, types
1384
+ await Promise.all([
1385
+ this.actionGenerateSchemas(),
1386
+ ...(form.entityId === undefined
1387
+ ? [
1388
+ this.generateTemplate("init_types", {
1389
+ entityId: form.entityId,
1390
+ }),
1391
+ ]
1392
+ : []),
1393
+ ]);
1352
1394
  }
1353
1395
 
1354
1396
  async delEntity(entityId: string): Promise<{ delPaths: string[] }> {
@@ -112,6 +112,20 @@ export class Template__generated_http extends Template {
112
112
  return zodType._def.items.map((item: any) =>
113
113
  this.zodTypeToReqDefault(item, name)
114
114
  );
115
+ } else if (zodType instanceof z.ZodDate) {
116
+ return "2000-01-01";
117
+ } else if (zodType instanceof z.ZodLiteral) {
118
+ return zodType.value;
119
+ } else if (zodType instanceof z.ZodEffects) {
120
+ return this.zodTypeToReqDefault(zodType._def.schema, name);
121
+ } else if (zodType instanceof z.ZodRecord || zodType instanceof z.ZodMap) {
122
+ const key = this.zodTypeToReqDefault(zodType._def.keyType, name) as any;
123
+ const value = this.zodTypeToReqDefault(zodType._def.valueType, name);
124
+ return { [key]: value };
125
+ } else if (zodType instanceof z.ZodSet) {
126
+ return [this.zodTypeToReqDefault(zodType._def.valueType, name)];
127
+ } else if (zodType instanceof z.ZodIntersection) {
128
+ return this.zodTypeToReqDefault(zodType._def.right, name);
115
129
  } else {
116
130
  // console.log(zodType);
117
131
  return `unknown-${zodType._type}`;
@@ -0,0 +1,205 @@
1
+ import {
2
+ EntityProp,
3
+ isBelongsToOneRelationProp,
4
+ isBigIntegerProp,
5
+ isBooleanProp,
6
+ isDateProp,
7
+ isDateTimeProp,
8
+ isDecimalProp,
9
+ isDoubleProp,
10
+ isEnumProp,
11
+ isFloatProp,
12
+ isIntegerProp,
13
+ isJsonProp,
14
+ isRelationProp,
15
+ isStringProp,
16
+ isTextProp,
17
+ isTimeProp,
18
+ isTimestampProp,
19
+ isUuidProp,
20
+ isVirtualProp,
21
+ } from "../types/types";
22
+ import { EntityManager } from "../entity/entity-manager";
23
+ import { Template } from "./base-template";
24
+ import { SourceCode } from "./generated.template";
25
+ import _ from "lodash";
26
+ import { nonNullable } from "../utils/utils";
27
+ import { Sonamu } from "../api";
28
+ import { Entity } from "../entity/entity";
29
+ import inflection from "inflection";
30
+ import { DB } from "../database/db";
31
+ import { KyselyBaseConfig } from "../database/types";
32
+
33
+ export class Template__kysely_interface extends Template {
34
+ constructor() {
35
+ super("kysely_interface");
36
+ }
37
+
38
+ getTargetAndPath() {
39
+ const { dir } = Sonamu.config.api;
40
+ const { types } = DB.baseConfig as KyselyBaseConfig;
41
+ const outDir = types?.outDir ?? "src/typings";
42
+ const fileName = types?.fileName ?? "database.types.ts";
43
+
44
+ return {
45
+ target: `${dir}/${outDir}`,
46
+ path: fileName,
47
+ };
48
+ }
49
+
50
+ render() {
51
+ const entityIds = EntityManager.getAllIds();
52
+ const entities = entityIds.map((id) => EntityManager.get(id));
53
+ const enums = _.merge({}, ...entities.map((e) => e.enums));
54
+
55
+ const manyToManyTables = _.uniq(
56
+ entities.flatMap((e) =>
57
+ e.props
58
+ .map((p) => {
59
+ if (isRelationProp(p) && p.relationType === "ManyToMany") {
60
+ return p.joinTable;
61
+ }
62
+ return null;
63
+ })
64
+ .filter(nonNullable)
65
+ )
66
+ ).map((table) => {
67
+ const [fromTable, toTable] = table.split("__");
68
+ return {
69
+ table,
70
+ fromTable,
71
+ toTable,
72
+ interfaceName: `${inflection.classify(fromTable)}${inflection.classify(toTable)}Table`,
73
+ };
74
+ });
75
+
76
+ const sourceCodes: Omit<SourceCode, "label">[] = entities.map((entity) => {
77
+ const columns = entity.props.map((prop) =>
78
+ this.resolveColumn(prop, enums)
79
+ );
80
+
81
+ return {
82
+ lines: [
83
+ `interface ${entity.id}Table {
84
+ ${columns.join("\n")}
85
+ }`,
86
+ "",
87
+ ],
88
+ importKeys: [],
89
+ };
90
+ });
91
+
92
+ sourceCodes.push(
93
+ ...manyToManyTables.map(({ fromTable, toTable, interfaceName }) => {
94
+ return {
95
+ lines: [
96
+ `interface ${interfaceName} {
97
+ id: number;
98
+ ${inflection.singularize(fromTable)}_id: number;
99
+ ${inflection.singularize(toTable)}_id: number;
100
+ }`,
101
+ "",
102
+ ],
103
+ importKeys: [],
104
+ };
105
+ })
106
+ );
107
+
108
+ const sourceCode = sourceCodes.reduce(
109
+ (result, ts) => {
110
+ if (ts === null) {
111
+ return result;
112
+ }
113
+ return {
114
+ lines: [...result!.lines, ...ts.lines, ""],
115
+ importKeys: _.uniq([...result!.importKeys, ...ts.importKeys]),
116
+ };
117
+ },
118
+ {
119
+ lines: [],
120
+ importKeys: [],
121
+ } as Omit<SourceCode, "label">
122
+ );
123
+
124
+ return {
125
+ ...this.getTargetAndPath(),
126
+ body: sourceCode.lines.join("\n"),
127
+ importKeys: sourceCode.importKeys,
128
+ customHeaders: [
129
+ `import { Generated, ColumnType } from "kysely";`,
130
+ "",
131
+ `export interface KyselyDatabase {
132
+ ${entities.map((entity) => `${entity.table}: ${entity.id}Table`).join(",\n")}
133
+ ${manyToManyTables.map(({ table, interfaceName }) => `${table}: ${interfaceName}`).join(",\n")}
134
+ }`,
135
+ "",
136
+ `declare module "sonamu" {
137
+ export interface DatabaseExtend extends KyselyDatabase {}
138
+ }`,
139
+ ],
140
+ };
141
+ }
142
+
143
+ private resolveColumn(prop: EntityProp, enums: Entity["enums"]) {
144
+ if (isVirtualProp(prop)) {
145
+ return null;
146
+ }
147
+
148
+ if (prop.name === "id") {
149
+ return "id: Generated<number>";
150
+ }
151
+
152
+ if (isRelationProp(prop)) {
153
+ if (isBelongsToOneRelationProp(prop)) {
154
+ return `${prop.name}_id: ${prop.nullable ? "number | null" : "number"}`;
155
+ }
156
+ return null;
157
+ }
158
+
159
+ let type: string;
160
+
161
+ if (isIntegerProp(prop)) {
162
+ type = "number";
163
+ } else if (isBigIntegerProp(prop)) {
164
+ type = "string";
165
+ } else if (isStringProp(prop) || isTextProp(prop)) {
166
+ type = "string";
167
+ } else if (isEnumProp(prop)) {
168
+ const enumValues = enums[prop.id];
169
+ if (!enumValues) {
170
+ console.warn(`Enum values not found for ${prop.id}`);
171
+ return null;
172
+ }
173
+ type = Object.keys(enumValues.Values)
174
+ .map((e) => `"${e}"`)
175
+ .join(" | ");
176
+ } else if (isFloatProp(prop) || isDoubleProp(prop) || isDecimalProp(prop)) {
177
+ type = "number";
178
+ } else if (isBooleanProp(prop)) {
179
+ type = "boolean";
180
+ } else if (
181
+ isDateProp(prop) ||
182
+ isDateTimeProp(prop) ||
183
+ isTimeProp(prop) ||
184
+ isTimestampProp(prop)
185
+ ) {
186
+ type = "string";
187
+ } else if (isJsonProp(prop)) {
188
+ type = "string";
189
+ } else if (isUuidProp(prop)) {
190
+ type = "string";
191
+ } else {
192
+ console.warn(`Unknown prop type: ${(prop as any).type}`);
193
+ type = "unknown";
194
+ }
195
+
196
+ if (prop.nullable) {
197
+ type = `${type} | null`;
198
+ }
199
+ if (prop.dbDefault) {
200
+ type = `ColumnType<${type}, ${type} | undefined, ${type}>`;
201
+ }
202
+
203
+ return `${prop.name}: ${type};`;
204
+ }
205
+ }
@@ -3,6 +3,7 @@ import { EntityManager, EntityNamesRecord } from "../entity/entity-manager";
3
3
  import { Template } from "./base-template";
4
4
  import { Template__view_list } from "./view_list.template";
5
5
  import { Sonamu } from "../api";
6
+ import { DB } from "../database/db";
6
7
 
7
8
  export class Template__model extends Template {
8
9
  constructor() {
@@ -24,7 +25,6 @@ export class Template__model extends Template {
24
25
  listParamsNode: RenderingNode
25
26
  ) {
26
27
  const names = EntityManager.getNamesFromId(entityId);
27
- const entity = EntityManager.get(entityId);
28
28
 
29
29
  const vlTpl = new Template__view_list();
30
30
  if (listParamsNode?.children === undefined) {
@@ -34,144 +34,7 @@ export class Template__model extends Template {
34
34
 
35
35
  return {
36
36
  ...this.getTargetAndPath(names),
37
- body: `
38
- import { BaseModelClass, ListResult, asArray, NotFoundException, BadRequestException, api } from 'sonamu';
39
- import {
40
- ${entityId}SubsetKey,
41
- ${entityId}SubsetMapping,
42
- } from "../sonamu.generated";
43
- import {
44
- ${names.camel}SubsetQueries,
45
- } from "../sonamu.generated.sso";
46
- import { ${entityId}ListParams, ${entityId}SaveParams } from "./${names.fs}.types";
47
-
48
- /*
49
- ${entityId} Model
50
- */
51
- class ${entityId}ModelClass extends BaseModelClass {
52
- modelName = "${entityId}";
53
-
54
- @api({ httpMethod: "GET", clients: ["axios", "swr"], resourceName: "${entityId}" })
55
- async findById<T extends ${entityId}SubsetKey>(
56
- subset: T,
57
- id: number
58
- ): Promise<${entityId}SubsetMapping[T]> {
59
- const { rows } = await this.findMany(subset, {
60
- id,
61
- num: 1,
62
- page: 1,
63
- });
64
- if (rows.length == 0) {
65
- throw new NotFoundException(\`존재하지 않는 ${names.capital} ID \${id}\`);
66
- }
67
-
68
- return rows[0];
69
- }
70
-
71
- async findOne<T extends ${entityId}SubsetKey>(
72
- subset: T,
73
- listParams: ${entityId}ListParams
74
- ): Promise<${entityId}SubsetMapping[T] | null> {
75
- const { rows } = await this.findMany(subset, {
76
- ...listParams,
77
- num: 1,
78
- page: 1,
79
- });
80
-
81
- return rows[0] ?? null;
82
- }
83
-
84
- @api({ httpMethod: "GET", clients: ["axios", "swr"], resourceName: "${names.capitalPlural}" })
85
- async findMany<T extends ${entityId}SubsetKey>(
86
- subset: T,
87
- params: ${entityId}ListParams = {}
88
- ): Promise<ListResult<${entityId}SubsetMapping[T]>> {
89
- // params with defaults
90
- params = {
91
- num: 24,
92
- page: 1,
93
- search: "${def.search}",
94
- orderBy: "${def.orderBy}",
95
- ...params,
96
- };
97
-
98
- // build queries
99
- let { rows, total } = await this.runSubsetQuery({
100
- subset,
101
- params,
102
- subsetQuery: ${names.camel}SubsetQueries[subset],
103
- build: ({ qb }) => {
104
- // id
105
- if (params.id) {
106
- qb.whereIn("${entity.table}.id", asArray(params.id));
107
- }
108
-
109
- // search-keyword
110
- if (params.search && params.keyword && params.keyword.length > 0) {
111
- if (params.search === "id") {
112
- qb.where("${entity.table}.id", params.keyword);
113
- // } else if (params.search === "field") {
114
- // qb.where("${entity.table}.field", "like", \`%\${params.keyword}%\`);
115
- } else {
116
- throw new BadRequestException(
117
- \`구현되지 않은 검색 필드 \${params.search}\`
118
- );
119
- }
120
- }
121
-
122
- // orderBy
123
- if (params.orderBy) {
124
- // default orderBy
125
- const [orderByField, orderByDirec] = params.orderBy.split("-");
126
- qb.orderBy("${entity.table}." + orderByField, orderByDirec);
127
- }
128
-
129
- return qb;
130
- },
131
- debug: false,
132
- });
133
-
134
- return {
135
- rows,
136
- total,
137
- };
138
- }
139
-
140
- @api({ httpMethod: "POST" })
141
- async save(
142
- spa: ${entityId}SaveParams[]
143
- ): Promise<number[]> {
144
- const wdb = this.getDB("w");
145
- const ub = this.getUpsertBuilder();
146
-
147
- // register
148
- spa.map((sp) => {
149
- ub.register("${entity.table}", sp);
150
- });
151
-
152
- // transaction
153
- return wdb.transaction(async (trx) => {
154
- const ids = await ub.upsert(trx, "${entity.table}");
155
-
156
- return ids;
157
- });
158
- }
159
-
160
- @api({ httpMethod: "POST", guards: [ "admin" ] })
161
- async del(ids: number[]): Promise<number> {
162
- const wdb = this.getDB("w");
163
-
164
- // transaction
165
- await wdb.transaction(async (trx) => {
166
- return trx("${entity.table}").whereIn("${entity.table}.id", ids).delete();
167
- });
168
-
169
- return ids.length;
170
- }
171
- }
172
-
173
- export const ${entityId}Model = new ${entityId}ModelClass();
174
- `.trim(),
37
+ body: DB.generator.generateModelTemplate(entityId, def),
175
38
  importKeys: [],
176
39
  };
177
40
  }
@@ -72,7 +72,9 @@ export class Template__service extends Template {
72
72
  const paramsWithoutContext = api.parameters.filter(
73
73
  (param) =>
74
74
  !ApiParamType.isContext(param.type) &&
75
- !ApiParamType.isRefKnex(param.type)
75
+ !ApiParamType.isRefKnex(param.type) &&
76
+ !ApiParamType.isRefKysely(param.type) &&
77
+ !(param.optional === true && param.name.startsWith("_")) // _로 시작하는 파라미터는 제외
76
78
  );
77
79
 
78
80
  // 파라미터 타입 정의
@@ -0,0 +1,111 @@
1
+ import { RelationNode, EntityProp, FixtureRecord } from "../types/types";
2
+ import { EntityManager } from "../entity/entity-manager";
3
+ import {
4
+ isRelationProp,
5
+ isBelongsToOneRelationProp,
6
+ isOneToOneRelationProp,
7
+ isManyToManyRelationProp,
8
+ } from "../types/types";
9
+
10
+ // 관계 그래프 처리를 별도 클래스로 분리
11
+ export class RelationGraph {
12
+ private graph: Map<string, RelationNode> = new Map();
13
+
14
+ buildGraph(fixtures: FixtureRecord[]): void {
15
+ this.graph.clear();
16
+
17
+ // 1. 노드 추가
18
+ for (const fixture of fixtures) {
19
+ this.graph.set(fixture.fixtureId, {
20
+ fixtureId: fixture.fixtureId,
21
+ entityId: fixture.entityId,
22
+ related: new Set(),
23
+ });
24
+ }
25
+
26
+ // 2. 의존성 추가
27
+ for (const fixture of fixtures) {
28
+ const node = this.graph.get(fixture.fixtureId)!;
29
+
30
+ for (const [, column] of Object.entries(fixture.columns)) {
31
+ const prop = column.prop as EntityProp;
32
+
33
+ if (isRelationProp(prop)) {
34
+ if (
35
+ isBelongsToOneRelationProp(prop) ||
36
+ (isOneToOneRelationProp(prop) && prop.hasJoinColumn)
37
+ ) {
38
+ const relatedFixtureId = `${prop.with}#${column.value}`;
39
+ if (this.graph.has(relatedFixtureId)) {
40
+ node.related.add(relatedFixtureId);
41
+ }
42
+ } else if (isManyToManyRelationProp(prop)) {
43
+ // ManyToMany 관계의 경우 양방향 의존성 추가
44
+ const relatedIds = column.value as number[];
45
+ for (const relatedId of relatedIds) {
46
+ const relatedFixtureId = `${prop.with}#${relatedId}`;
47
+ if (this.graph.has(relatedFixtureId)) {
48
+ node.related.add(relatedFixtureId);
49
+ this.graph
50
+ .get(relatedFixtureId)!
51
+ .related.add(fixture.fixtureId);
52
+ }
53
+ }
54
+ }
55
+ }
56
+ }
57
+ }
58
+ }
59
+
60
+ getInsertionOrder(): string[] {
61
+ const visited = new Set<string>();
62
+ const order: string[] = [];
63
+ const tempVisited = new Set<string>();
64
+
65
+ const visit = (fixtureId: string) => {
66
+ if (visited.has(fixtureId)) return;
67
+ if (tempVisited.has(fixtureId)) {
68
+ console.warn(`Circular dependency detected involving: ${fixtureId}`);
69
+ return;
70
+ }
71
+
72
+ tempVisited.add(fixtureId);
73
+
74
+ const node = this.graph.get(fixtureId)!;
75
+ const entity = EntityManager.get(node.entityId);
76
+
77
+ for (const depId of node.related) {
78
+ const depNode = this.graph.get(depId)!;
79
+
80
+ // BelongsToOne 관계이면서 nullable이 아닌 경우 먼저 방문
81
+ const relationProp = entity.props.find(
82
+ (prop) =>
83
+ isRelationProp(prop) &&
84
+ (isBelongsToOneRelationProp(prop) ||
85
+ (isOneToOneRelationProp(prop) && prop.hasJoinColumn)) &&
86
+ prop.with === depNode.entityId
87
+ );
88
+ if (relationProp && !relationProp.nullable) {
89
+ visit(depId);
90
+ }
91
+ }
92
+
93
+ tempVisited.delete(fixtureId);
94
+ visited.add(fixtureId);
95
+ order.push(fixtureId);
96
+ };
97
+
98
+ for (const fixtureId of this.graph.keys()) {
99
+ visit(fixtureId);
100
+ }
101
+
102
+ // circular dependency로 인해 방문되지 않은 fixtureId 추가
103
+ for (const fixtureId of this.graph.keys()) {
104
+ if (!visited.has(fixtureId)) {
105
+ order.push(fixtureId);
106
+ }
107
+ }
108
+
109
+ return order;
110
+ }
111
+ }