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.
- package/.pnp.cjs +11 -0
- package/dist/base-model-BzMJ2E_I.d.mts +43 -0
- package/dist/base-model-CWRKUX49.d.ts +43 -0
- package/dist/bin/cli.js +118 -89
- package/dist/bin/cli.js.map +1 -1
- package/dist/bin/cli.mjs +74 -45
- package/dist/bin/cli.mjs.map +1 -1
- package/dist/chunk-FLPD24HS.mjs +231 -0
- package/dist/chunk-FLPD24HS.mjs.map +1 -0
- package/dist/chunk-I2MMJRJN.mjs +1550 -0
- package/dist/chunk-I2MMJRJN.mjs.map +1 -0
- package/dist/{chunk-MPXE4IHO.mjs → chunk-PP2PSSAG.mjs} +5284 -5617
- package/dist/chunk-PP2PSSAG.mjs.map +1 -0
- package/dist/chunk-QK5XXJUX.mjs +280 -0
- package/dist/chunk-QK5XXJUX.mjs.map +1 -0
- package/dist/chunk-U636LQJJ.js +231 -0
- package/dist/chunk-U636LQJJ.js.map +1 -0
- package/dist/chunk-W7KDVJLQ.js +280 -0
- package/dist/chunk-W7KDVJLQ.js.map +1 -0
- package/dist/{chunk-YXILRRDT.js → chunk-XT6LHCX5.js} +5252 -5585
- package/dist/chunk-XT6LHCX5.js.map +1 -0
- package/dist/chunk-Z2P7XTXE.js +1550 -0
- package/dist/chunk-Z2P7XTXE.js.map +1 -0
- package/dist/database/drivers/knex/base-model.d.mts +16 -0
- package/dist/database/drivers/knex/base-model.d.ts +16 -0
- package/dist/database/drivers/knex/base-model.js +55 -0
- package/dist/database/drivers/knex/base-model.js.map +1 -0
- package/dist/database/drivers/knex/base-model.mjs +56 -0
- package/dist/database/drivers/knex/base-model.mjs.map +1 -0
- package/dist/database/drivers/kysely/base-model.d.mts +22 -0
- package/dist/database/drivers/kysely/base-model.d.ts +22 -0
- package/dist/database/drivers/kysely/base-model.js +64 -0
- package/dist/database/drivers/kysely/base-model.js.map +1 -0
- package/dist/database/drivers/kysely/base-model.mjs +65 -0
- package/dist/database/drivers/kysely/base-model.mjs.map +1 -0
- package/dist/index.d.mts +220 -926
- package/dist/index.d.ts +220 -926
- package/dist/index.js +13 -26
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +18 -31
- package/dist/index.mjs.map +1 -1
- package/dist/model-CAH_4oQh.d.mts +1042 -0
- package/dist/model-CAH_4oQh.d.ts +1042 -0
- package/import-to-require.js +27 -0
- package/package.json +23 -2
- package/src/api/caster.ts +6 -0
- package/src/api/code-converters.ts +3 -1
- package/src/api/sonamu.ts +41 -22
- package/src/bin/cli.ts +78 -46
- package/src/database/_batch_update.ts +16 -11
- package/src/database/base-model.abstract.ts +97 -0
- package/src/database/base-model.ts +214 -280
- package/src/database/code-generator.ts +72 -0
- package/src/database/db.abstract.ts +75 -0
- package/src/database/db.ts +21 -82
- package/src/database/drivers/knex/base-model.ts +55 -0
- package/src/database/drivers/knex/client.ts +209 -0
- package/src/database/drivers/knex/db.ts +227 -0
- package/src/database/drivers/knex/generator.ts +659 -0
- package/src/database/drivers/kysely/base-model.ts +89 -0
- package/src/database/drivers/kysely/client.ts +309 -0
- package/src/database/drivers/kysely/db.ts +238 -0
- package/src/database/drivers/kysely/generator.ts +714 -0
- package/src/database/types.ts +117 -0
- package/src/database/upsert-builder.ts +31 -18
- package/src/entity/entity-utils.ts +1 -1
- package/src/entity/migrator.ts +98 -693
- package/src/index.ts +1 -1
- package/src/syncer/syncer.ts +69 -27
- package/src/templates/generated_http.template.ts +14 -0
- package/src/templates/kysely_types.template.ts +205 -0
- package/src/templates/model.template.ts +2 -139
- package/src/templates/service.template.ts +3 -1
- package/src/testing/_relation-graph.ts +111 -0
- package/src/testing/fixture-manager.ts +216 -332
- package/src/types/types.ts +56 -6
- package/src/utils/utils.ts +56 -4
- package/src/utils/zod-error.ts +189 -0
- package/tsconfig.json +2 -2
- package/tsup.config.js +11 -10
- package/dist/chunk-MPXE4IHO.mjs.map +0 -1
- package/dist/chunk-YXILRRDT.js.map +0 -1
- /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";
|
package/src/syncer/syncer.ts
CHANGED
|
@@ -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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
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
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
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
|
+
}
|