sonamu 0.0.42 → 0.1.2

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 (208) hide show
  1. package/.pnp.cjs +1963 -951
  2. package/.pnp.loader.mjs +1816 -54
  3. package/.yarnrc.yml +1 -1
  4. package/dist/api/code-converters.d.ts +3 -3
  5. package/dist/api/code-converters.d.ts.map +1 -1
  6. package/dist/api/code-converters.js +5 -15
  7. package/dist/api/code-converters.js.map +1 -1
  8. package/dist/api/context.d.ts +1 -1
  9. package/dist/api/context.d.ts.map +1 -1
  10. package/dist/api/decorators.d.ts +3 -3
  11. package/dist/api/decorators.d.ts.map +1 -1
  12. package/dist/api/sonamu.d.ts +3 -3
  13. package/dist/api/sonamu.d.ts.map +1 -1
  14. package/dist/api/sonamu.js +6 -6
  15. package/dist/api/sonamu.js.map +1 -1
  16. package/dist/bin/cli.js +132 -33
  17. package/dist/bin/cli.js.map +1 -1
  18. package/dist/database/db.d.ts +2 -2
  19. package/dist/database/db.d.ts.map +1 -1
  20. package/dist/database/db.js +1 -1
  21. package/dist/database/db.js.map +1 -1
  22. package/dist/database/upsert-builder.d.ts +2 -2
  23. package/dist/database/upsert-builder.d.ts.map +1 -1
  24. package/dist/database/upsert-builder.js +10 -8
  25. package/dist/database/upsert-builder.js.map +1 -1
  26. package/dist/entity/entity-manager.d.ts +29 -0
  27. package/dist/entity/entity-manager.d.ts.map +1 -0
  28. package/dist/entity/entity-manager.js +128 -0
  29. package/dist/entity/entity-manager.js.map +1 -0
  30. package/dist/entity/entity-utils.d.ts +61 -0
  31. package/dist/entity/entity-utils.d.ts.map +1 -0
  32. package/dist/entity/entity-utils.js +121 -0
  33. package/dist/entity/entity-utils.js.map +1 -0
  34. package/dist/entity/entity.d.ts +54 -0
  35. package/dist/entity/entity.d.ts.map +1 -0
  36. package/dist/entity/entity.js +596 -0
  37. package/dist/entity/entity.js.map +1 -0
  38. package/dist/entity/migrator.d.ts +143 -0
  39. package/dist/entity/migrator.d.ts.map +1 -0
  40. package/dist/entity/migrator.js +1385 -0
  41. package/dist/entity/migrator.js.map +1 -0
  42. package/dist/entity/smd-utils.d.ts +61 -0
  43. package/dist/entity/smd-utils.d.ts.map +1 -0
  44. package/dist/entity/smd-utils.js +121 -0
  45. package/dist/entity/smd-utils.js.map +1 -0
  46. package/dist/index.d.ts +3 -3
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +3 -3
  49. package/dist/index.js.map +1 -1
  50. package/dist/smd/entity-manager.d.ts +28 -0
  51. package/dist/smd/entity-manager.d.ts.map +1 -0
  52. package/dist/smd/entity-manager.js +119 -0
  53. package/dist/smd/entity-manager.js.map +1 -0
  54. package/dist/smd/entity.d.ts +40 -0
  55. package/dist/smd/entity.d.ts.map +1 -0
  56. package/dist/smd/entity.js +430 -0
  57. package/dist/smd/entity.js.map +1 -0
  58. package/dist/smd/migrator.d.ts +2 -2
  59. package/dist/smd/migrator.d.ts.map +1 -1
  60. package/dist/smd/migrator.js +5 -5
  61. package/dist/smd/migrator.js.map +1 -1
  62. package/dist/smd/smd-manager.d.ts +3 -3
  63. package/dist/smd/smd-manager.d.ts.map +1 -1
  64. package/dist/smd/smd-manager.js +2 -2
  65. package/dist/smd/smd-manager.js.map +1 -1
  66. package/dist/smd/smd-utils.d.ts +3 -3
  67. package/dist/smd/smd-utils.d.ts.map +1 -1
  68. package/dist/smd/smd.d.ts +5 -6
  69. package/dist/smd/smd.d.ts.map +1 -1
  70. package/dist/smd/smd.js +3 -3
  71. package/dist/smd/smd.js.map +1 -1
  72. package/dist/syncer/syncer.d.ts +15 -11
  73. package/dist/syncer/syncer.d.ts.map +1 -1
  74. package/dist/syncer/syncer.js +134 -74
  75. package/dist/syncer/syncer.js.map +1 -1
  76. package/dist/templates/base-template.d.ts +2 -2
  77. package/dist/templates/base-template.d.ts.map +1 -1
  78. package/dist/templates/entity.template.d.ts +17 -0
  79. package/dist/templates/entity.template.d.ts.map +1 -0
  80. package/dist/templates/entity.template.js +46 -0
  81. package/dist/templates/entity.template.js.map +1 -0
  82. package/dist/templates/generated.template.d.ts +11 -7
  83. package/dist/templates/generated.template.d.ts.map +1 -1
  84. package/dist/templates/generated.template.js +72 -43
  85. package/dist/templates/generated.template.js.map +1 -1
  86. package/dist/templates/generated_http.template.d.ts +3 -3
  87. package/dist/templates/generated_http.template.d.ts.map +1 -1
  88. package/dist/templates/generated_http.template.js +3 -3
  89. package/dist/templates/generated_http.template.js.map +1 -1
  90. package/dist/templates/init_enums.template.d.ts +2 -2
  91. package/dist/templates/init_enums.template.d.ts.map +1 -1
  92. package/dist/templates/init_enums.template.js +2 -2
  93. package/dist/templates/init_enums.template.js.map +1 -1
  94. package/dist/templates/init_generated.template.d.ts +3 -3
  95. package/dist/templates/init_generated.template.d.ts.map +1 -1
  96. package/dist/templates/init_generated.template.js +13 -14
  97. package/dist/templates/init_generated.template.js.map +1 -1
  98. package/dist/templates/init_types.template.d.ts +3 -3
  99. package/dist/templates/init_types.template.d.ts.map +1 -1
  100. package/dist/templates/init_types.template.js +10 -10
  101. package/dist/templates/init_types.template.js.map +1 -1
  102. package/dist/templates/model.template.d.ts +3 -3
  103. package/dist/templates/model.template.d.ts.map +1 -1
  104. package/dist/templates/model.template.js +28 -28
  105. package/dist/templates/model.template.js.map +1 -1
  106. package/dist/templates/model_test.template.d.ts +3 -3
  107. package/dist/templates/model_test.template.d.ts.map +1 -1
  108. package/dist/templates/model_test.template.js +4 -4
  109. package/dist/templates/model_test.template.js.map +1 -1
  110. package/dist/templates/service.template.d.ts +3 -3
  111. package/dist/templates/service.template.d.ts.map +1 -1
  112. package/dist/templates/service.template.js +3 -3
  113. package/dist/templates/service.template.js.map +1 -1
  114. package/dist/templates/smd.template.d.ts +2 -2
  115. package/dist/templates/smd.template.d.ts.map +1 -1
  116. package/dist/templates/smd.template.js +2 -2
  117. package/dist/templates/smd.template.js.map +1 -1
  118. package/dist/templates/view_enums_buttonset.template.d.ts +3 -3
  119. package/dist/templates/view_enums_buttonset.template.d.ts.map +1 -1
  120. package/dist/templates/view_enums_buttonset.template.js +4 -4
  121. package/dist/templates/view_enums_buttonset.template.js.map +1 -1
  122. package/dist/templates/view_enums_dropdown.template.d.ts +3 -3
  123. package/dist/templates/view_enums_dropdown.template.d.ts.map +1 -1
  124. package/dist/templates/view_enums_dropdown.template.js +3 -3
  125. package/dist/templates/view_enums_dropdown.template.js.map +1 -1
  126. package/dist/templates/view_enums_select.template.d.ts +3 -3
  127. package/dist/templates/view_enums_select.template.d.ts.map +1 -1
  128. package/dist/templates/view_enums_select.template.js +3 -3
  129. package/dist/templates/view_enums_select.template.js.map +1 -1
  130. package/dist/templates/view_form.template.d.ts +25 -29
  131. package/dist/templates/view_form.template.d.ts.map +1 -1
  132. package/dist/templates/view_form.template.js +19 -19
  133. package/dist/templates/view_form.template.js.map +1 -1
  134. package/dist/templates/view_id_all_select.template.d.ts +3 -3
  135. package/dist/templates/view_id_all_select.template.d.ts.map +1 -1
  136. package/dist/templates/view_id_all_select.template.js +4 -4
  137. package/dist/templates/view_id_all_select.template.js.map +1 -1
  138. package/dist/templates/view_id_async_select.template.d.ts +3 -3
  139. package/dist/templates/view_id_async_select.template.d.ts.map +1 -1
  140. package/dist/templates/view_id_async_select.template.js +6 -6
  141. package/dist/templates/view_id_async_select.template.js.map +1 -1
  142. package/dist/templates/view_list.template.d.ts +30 -34
  143. package/dist/templates/view_list.template.d.ts.map +1 -1
  144. package/dist/templates/view_list.template.js +40 -40
  145. package/dist/templates/view_list.template.js.map +1 -1
  146. package/dist/templates/view_list_columns.template.d.ts +3 -3
  147. package/dist/templates/view_list_columns.template.d.ts.map +1 -1
  148. package/dist/templates/view_list_columns.template.js +3 -3
  149. package/dist/templates/view_list_columns.template.js.map +1 -1
  150. package/dist/templates/view_search_input.template.d.ts +3 -3
  151. package/dist/templates/view_search_input.template.d.ts.map +1 -1
  152. package/dist/templates/view_search_input.template.js +3 -3
  153. package/dist/templates/view_search_input.template.js.map +1 -1
  154. package/dist/testing/fixture-manager.d.ts +2 -2
  155. package/dist/testing/fixture-manager.d.ts.map +1 -1
  156. package/dist/testing/fixture-manager.js +18 -16
  157. package/dist/testing/fixture-manager.js.map +1 -1
  158. package/dist/types/smd.types.d.ts +741 -0
  159. package/dist/types/smd.types.d.ts.map +1 -0
  160. package/dist/types/smd.types.js +292 -0
  161. package/dist/types/smd.types.js.map +1 -0
  162. package/dist/types/types.d.ts +187 -190
  163. package/dist/types/types.d.ts.map +1 -1
  164. package/dist/types/types.js +22 -30
  165. package/dist/types/types.js.map +1 -1
  166. package/dist/utils/model.d.ts +2 -2
  167. package/dist/utils/model.d.ts.map +1 -1
  168. package/dist/utils/utils.d.ts +1 -0
  169. package/dist/utils/utils.d.ts.map +1 -1
  170. package/dist/utils/utils.js +6 -2
  171. package/dist/utils/utils.js.map +1 -1
  172. package/package.json +11 -8
  173. package/src/api/code-converters.ts +9 -17
  174. package/src/api/sonamu.ts +10 -6
  175. package/src/bin/cli.ts +156 -27
  176. package/src/database/upsert-builder.ts +2 -2
  177. package/src/entity/entity-manager.ts +150 -0
  178. package/src/{smd/smd-utils.ts → entity/entity-utils.ts} +3 -3
  179. package/src/entity/entity.ts +774 -0
  180. package/src/{smd → entity}/migrator.ts +426 -106
  181. package/src/index.ts +3 -3
  182. package/src/smd/smd-manager.ts +3 -13
  183. package/src/smd/smd.ts +13 -10
  184. package/src/syncer/syncer.ts +125 -73
  185. package/src/templates/base-template.ts +2 -2
  186. package/src/templates/entity.template.ts +50 -0
  187. package/src/templates/generated.template.ts +93 -57
  188. package/src/templates/generated_http.template.ts +4 -4
  189. package/src/templates/init_types.template.ts +11 -11
  190. package/src/templates/model.template.ts +29 -29
  191. package/src/templates/model_test.template.ts +5 -5
  192. package/src/templates/service.template.ts +4 -4
  193. package/src/templates/view_enums_buttonset.template.ts +5 -5
  194. package/src/templates/view_enums_dropdown.template.ts +4 -4
  195. package/src/templates/view_enums_select.template.ts +8 -4
  196. package/src/templates/view_form.template.ts +21 -21
  197. package/src/templates/view_id_all_select.template.ts +5 -5
  198. package/src/templates/view_id_async_select.template.ts +9 -7
  199. package/src/templates/view_list.template.ts +54 -44
  200. package/src/templates/view_list_columns.template.ts +4 -4
  201. package/src/templates/view_search_input.template.ts +4 -4
  202. package/src/testing/fixture-manager.ts +12 -12
  203. package/src/types/types.ts +59 -39
  204. package/src/utils/utils.ts +4 -0
  205. package/tsconfig.json +4 -1
  206. package/src/templates/init_enums.template.ts +0 -71
  207. package/src/templates/init_generated.template.ts +0 -51
  208. package/src/templates/smd.template.ts +0 -53
@@ -0,0 +1,774 @@
1
+ import _, { uniq } from "lodash";
2
+ import { EntityManager as EntityManager } from "./entity-manager";
3
+ import { dasherize, pluralize, underscore } from "inflection";
4
+ import {
5
+ EntityProp,
6
+ RelationProp,
7
+ isRelationProp,
8
+ SubsetQuery,
9
+ isVirtualProp,
10
+ isBelongsToOneRelationProp,
11
+ isOneToOneRelationProp,
12
+ isHasManyRelationProp,
13
+ isManyToManyRelationProp,
14
+ EntityPropNode,
15
+ isEnumProp,
16
+ StringProp,
17
+ EntityIndex,
18
+ EntityJson,
19
+ EntitySubsetRow,
20
+ } from "../types/types";
21
+ import inflection from "inflection";
22
+ import path from "path";
23
+ import { existsSync, writeFileSync } from "fs";
24
+ import { z } from "zod";
25
+ import { Sonamu } from "../api/sonamu";
26
+ import prettier from "prettier";
27
+
28
+ export class Entity {
29
+ id: string;
30
+ parentId?: string;
31
+ table: string;
32
+ title: string;
33
+ names: {
34
+ parentFs: string;
35
+ fs: string;
36
+ module: string;
37
+ };
38
+ props: EntityProp[];
39
+ propsDict: {
40
+ [key: string]: EntityProp;
41
+ };
42
+ relations: {
43
+ [key: string]: RelationProp;
44
+ };
45
+ indexes: EntityIndex[];
46
+ subsets: {
47
+ [key: string]: string[];
48
+ };
49
+ types: {
50
+ [name: string]: z.ZodTypeAny;
51
+ } = {};
52
+ enums: {
53
+ [enumId: string]: z.ZodEnum<any>;
54
+ } = {};
55
+ enumLabels: {
56
+ [enumId: string]: {
57
+ [key: string]: string;
58
+ };
59
+ } = {};
60
+
61
+ constructor({
62
+ id,
63
+ parentId,
64
+ table,
65
+ title,
66
+ props,
67
+ indexes,
68
+ subsets,
69
+ enums,
70
+ }: EntityJson) {
71
+ // id
72
+ this.id = id;
73
+ this.parentId = parentId;
74
+ this.title = title ?? this.id;
75
+ this.table = table ?? underscore(pluralize(id));
76
+
77
+ // props
78
+ if (props) {
79
+ this.props = props.map((prop) => {
80
+ if (isEnumProp(prop)) {
81
+ if (prop.id.includes("$Model")) {
82
+ prop.id = prop.id.replace("$Model", id);
83
+ }
84
+ }
85
+ return prop;
86
+ });
87
+ this.propsDict = props.reduce((result, prop) => {
88
+ return {
89
+ ...result,
90
+ [prop.name]: prop,
91
+ };
92
+ }, {});
93
+
94
+ // relations
95
+ this.relations = props
96
+ .filter((prop) => isRelationProp(prop))
97
+ .reduce((result, prop) => {
98
+ return {
99
+ ...result,
100
+ [prop.name]: prop,
101
+ };
102
+ }, {});
103
+ } else {
104
+ this.props = [];
105
+ this.propsDict = {};
106
+ this.relations = {};
107
+ }
108
+
109
+ // indexes
110
+ this.indexes = indexes ?? [];
111
+
112
+ // subsets
113
+ this.subsets = subsets ?? {};
114
+
115
+ // enums
116
+ this.enumLabels = enums ?? {};
117
+ this.enums = Object.fromEntries(
118
+ Object.entries(this.enumLabels).map(([key, enumLabel]) => {
119
+ return [
120
+ key,
121
+ z.enum(
122
+ Object.keys(enumLabel) as unknown as readonly [string, ...string[]]
123
+ ),
124
+ ];
125
+ })
126
+ );
127
+
128
+ // names
129
+ this.names = {
130
+ parentFs: dasherize(underscore(parentId ?? id)).toLowerCase(),
131
+ fs: dasherize(underscore(id)).toLowerCase(),
132
+ module: id,
133
+ };
134
+
135
+ this.registerModulePaths();
136
+ this.registerTableSpecs();
137
+ }
138
+
139
+ /*
140
+ subset SELECT/JOIN/LOADER 결과 리턴
141
+ */
142
+ getSubsetQuery(subsetKey: string): SubsetQuery {
143
+ const subset = this.subsets[subsetKey];
144
+
145
+ const result: SubsetQuery = this.resolveSubsetQuery("", subset);
146
+ return result;
147
+ }
148
+
149
+ /*
150
+ */
151
+ resolveSubsetQuery(
152
+ prefix: string,
153
+ fields: string[],
154
+ isAlreadyOuterJoined: boolean = false
155
+ ): SubsetQuery {
156
+ // prefix 치환 (prefix는 ToOneRelation이 복수로 붙은 경우 모두 __로 변경됨)
157
+ prefix = prefix.replace(/\./g, "__");
158
+
159
+ // 서브셋을 1뎁스만 분리하여 그룹핑
160
+ const subsetGroup = _.groupBy(fields, (field) => {
161
+ if (field.includes(".")) {
162
+ const [rel] = field.split(".");
163
+ return rel;
164
+ } else {
165
+ return "";
166
+ }
167
+ });
168
+
169
+ const result = Object.keys(subsetGroup).reduce(
170
+ (r, groupKey) => {
171
+ const fields = subsetGroup[groupKey];
172
+ // 현재 테이블 필드셋은 select, virtual에 추가하고 리턴
173
+ if (groupKey === "") {
174
+ const realFields = fields.filter(
175
+ (field) => !isVirtualProp(this.propsDict[field])
176
+ );
177
+ const virtualFields = fields.filter((field) =>
178
+ isVirtualProp(this.propsDict[field])
179
+ );
180
+
181
+ if (prefix === "") {
182
+ // 현재 테이블인 경우
183
+ r.select = r.select.concat(
184
+ realFields.map((field) => `${this.table}.${field}`)
185
+ );
186
+ r.virtual = r.virtual.concat(virtualFields);
187
+ } else {
188
+ // 넘어온 테이블인 경우
189
+ r.select = r.select.concat(
190
+ realFields.map(
191
+ (field) => `${prefix}.${field} as ${prefix}__${field}`
192
+ )
193
+ );
194
+ }
195
+
196
+ return r;
197
+ }
198
+
199
+ const relation = this.relations[groupKey];
200
+ if (relation === undefined) {
201
+ throw new Error(`존재하지 않는 relation 참조 ${groupKey}`);
202
+ }
203
+ const relEntity = EntityManager.get(relation.with);
204
+
205
+ if (
206
+ isOneToOneRelationProp(relation) ||
207
+ isBelongsToOneRelationProp(relation)
208
+ ) {
209
+ // -One Relation: JOIN 으로 처리
210
+ const relFields = fields.map((field) =>
211
+ field.split(".").slice(1).join(".")
212
+ );
213
+
214
+ // -One Relation에서 id 필드만 참조하는 경우 릴레이션 넘기지 않고 리턴
215
+ if (relFields.length === 1 && relFields[0] === "id") {
216
+ if (prefix === "") {
217
+ r.select = r.select.concat(`${this.table}.${groupKey}_id`);
218
+ } else {
219
+ r.select = r.select.concat(
220
+ `${prefix}.${groupKey}_id as ${prefix}__${groupKey}_id`
221
+ );
222
+ }
223
+ return r;
224
+ }
225
+
226
+ // innerOrOuter
227
+ const innerOrOuter = (() => {
228
+ if (isAlreadyOuterJoined) {
229
+ return "outer";
230
+ }
231
+
232
+ if (isOneToOneRelationProp(relation)) {
233
+ if (
234
+ relation.hasJoinColumn === true &&
235
+ (relation.nullable ?? false) === false
236
+ ) {
237
+ return "inner";
238
+ } else {
239
+ return "outer";
240
+ }
241
+ } else {
242
+ if (relation.nullable) {
243
+ return "outer";
244
+ } else {
245
+ return "inner";
246
+ }
247
+ }
248
+ })();
249
+ const relSubsetQuery = relEntity.resolveSubsetQuery(
250
+ `${prefix !== "" ? prefix + "." : ""}${groupKey}`,
251
+ relFields,
252
+ innerOrOuter === "outer"
253
+ );
254
+ r.select = r.select.concat(relSubsetQuery.select);
255
+ r.virtual = r.virtual.concat(relSubsetQuery.virtual);
256
+
257
+ const joinAs = prefix === "" ? groupKey : prefix + "__" + groupKey;
258
+ const fromTable = prefix === "" ? this.table : prefix;
259
+
260
+ let joinClause;
261
+ if (relation.customJoinClause) {
262
+ joinClause = {
263
+ custom: relation.customJoinClause,
264
+ };
265
+ } else {
266
+ let from, to;
267
+ if (isOneToOneRelationProp(relation)) {
268
+ if (relation.hasJoinColumn) {
269
+ from = `${fromTable}.${relation.name}_id`;
270
+ to = `${joinAs}.id`;
271
+ } else {
272
+ from = `${fromTable}.id`;
273
+ to = `${joinAs}.${underscore(
274
+ this.names.fs.replace(/\-/g, "_")
275
+ )}_id`;
276
+ }
277
+ } else {
278
+ from = `${fromTable}.${relation.name}_id`;
279
+ to = `${joinAs}.id`;
280
+ }
281
+ joinClause = {
282
+ from,
283
+ to,
284
+ };
285
+ }
286
+
287
+ r.joins.push({
288
+ as: joinAs,
289
+ join: innerOrOuter,
290
+ table: relEntity.table,
291
+ ...joinClause,
292
+ });
293
+
294
+ // BelongsToOne 밑에 HasMany가 붙은 경우
295
+ if (relSubsetQuery.loaders.length > 0) {
296
+ const convertedLoaders = relSubsetQuery.loaders.map((loader) => {
297
+ const newAs = [groupKey, loader.as].join("__");
298
+ return {
299
+ as: newAs,
300
+ table: loader.table,
301
+ manyJoin: loader.manyJoin,
302
+ oneJoins: loader.oneJoins,
303
+ select: loader.select,
304
+ };
305
+ });
306
+
307
+ r.loaders = [...r.loaders, ...convertedLoaders];
308
+ }
309
+
310
+ r.joins = r.joins.concat(relSubsetQuery.joins);
311
+ } else if (
312
+ isHasManyRelationProp(relation) ||
313
+ isManyToManyRelationProp(relation)
314
+ ) {
315
+ // -Many Relation: Loader 로 처리
316
+ const relFields = fields.map((field) =>
317
+ field.split(".").slice(1).join(".")
318
+ );
319
+ const relSubsetQuery = relEntity.resolveSubsetQuery("", relFields);
320
+
321
+ let manyJoin: SubsetQuery["loaders"][number]["manyJoin"];
322
+ if (isHasManyRelationProp(relation)) {
323
+ manyJoin = {
324
+ fromTable: this.table,
325
+ fromCol: "id",
326
+ idField: prefix === "" ? `id` : `${prefix}__id`,
327
+ toTable: relEntity.table,
328
+ toCol: relation.joinColumn,
329
+ };
330
+ } else if (isManyToManyRelationProp(relation)) {
331
+ const [table1, table2] = relation.joinTable.split("__");
332
+
333
+ manyJoin = {
334
+ fromTable: this.table,
335
+ fromCol: "id",
336
+ idField: prefix === "" ? `id` : `${prefix}__id`,
337
+ through: {
338
+ table: relation.joinTable,
339
+ fromCol: `${inflection.singularize(table1)}_id`,
340
+ toCol: `${inflection.singularize(table2)}_id`,
341
+ },
342
+ toTable: relEntity.table,
343
+ toCol: "id",
344
+ };
345
+ } else {
346
+ throw new Error();
347
+ }
348
+
349
+ r.loaders.push({
350
+ as: groupKey,
351
+ table: relEntity.table,
352
+ manyJoin,
353
+ oneJoins: relSubsetQuery.joins,
354
+ select: relSubsetQuery.select,
355
+ loaders: relSubsetQuery.loaders,
356
+ });
357
+ }
358
+
359
+ return r;
360
+ },
361
+ {
362
+ select: [],
363
+ virtual: [],
364
+ joins: [],
365
+ loaders: [],
366
+ } as SubsetQuery
367
+ );
368
+ return result;
369
+ }
370
+
371
+ /*
372
+ FieldExpr[] 을 EntityPropNode[] 로 변환
373
+ */
374
+ fieldExprsToPropNodes(
375
+ fieldExprs: string[],
376
+ entity: Entity = this
377
+ ): EntityPropNode[] {
378
+ const groups = fieldExprs.reduce(
379
+ (result, fieldExpr) => {
380
+ let key, value, elseExpr;
381
+ if (fieldExpr.includes(".")) {
382
+ [key, ...elseExpr] = fieldExpr.split(".");
383
+ value = elseExpr.join(".");
384
+ } else {
385
+ key = "";
386
+ value = fieldExpr;
387
+ }
388
+ result[key] = (result[key] ?? []).concat(value);
389
+
390
+ return result;
391
+ },
392
+ {} as {
393
+ [k: string]: string[];
394
+ }
395
+ );
396
+
397
+ return Object.keys(groups)
398
+ .map((key) => {
399
+ const group = groups[key];
400
+
401
+ // 일반 prop 처리
402
+ if (key === "") {
403
+ return group.map((propName) => {
404
+ // uuid 개별 처리
405
+ if (propName === "uuid") {
406
+ return {
407
+ nodeType: "plain" as const,
408
+ prop: {
409
+ type: "string",
410
+ name: "uuid",
411
+ length: 128,
412
+ } as StringProp,
413
+ children: [],
414
+ };
415
+ }
416
+
417
+ const prop = entity.propsDict[propName];
418
+ if (prop === undefined) {
419
+ throw new Error(`${this.id} -- 잘못된 FieldExpr ${propName}`);
420
+ }
421
+ return {
422
+ nodeType: "plain" as const,
423
+ prop,
424
+ children: [],
425
+ };
426
+ });
427
+ }
428
+
429
+ // relation prop 처리
430
+ const prop = entity.propsDict[key];
431
+ if (!isRelationProp(prop)) {
432
+ throw new Error(`잘못된 FieldExpr ${key}.${group[0]}`);
433
+ }
434
+ const relEntity = EntityManager.get(prop.with);
435
+
436
+ // relation -One 에 id 필드 하나인 경우
437
+ if (isBelongsToOneRelationProp(prop) || isOneToOneRelationProp(prop)) {
438
+ if (group.length == 1 && (group[0] === "id" || group[0] == "id?")) {
439
+ // id 하나만 있는지 체크해서, 하나만 있으면 상위 prop으로 id를 리턴
440
+ const idProp = relEntity.propsDict.id;
441
+ return {
442
+ nodeType: "plain" as const,
443
+ prop: {
444
+ ...idProp,
445
+ name: key + "_id",
446
+ nullable: prop.nullable,
447
+ },
448
+ children: [],
449
+ };
450
+ }
451
+ }
452
+
453
+ // -One 그외의 경우 object로 리턴
454
+ // -Many의 경우 array로 리턴
455
+ // Recursive 로 뎁스 처리
456
+ const children = this.fieldExprsToPropNodes(group, relEntity);
457
+ const nodeType =
458
+ isBelongsToOneRelationProp(prop) || isOneToOneRelationProp(prop)
459
+ ? ("object" as const)
460
+ : ("array" as const);
461
+
462
+ return {
463
+ prop,
464
+ children,
465
+ nodeType,
466
+ };
467
+ })
468
+ .flat();
469
+ }
470
+
471
+ getFieldExprs(
472
+ prefix = "",
473
+ maxDepth: number = 3,
474
+ froms: string[] = []
475
+ ): string[] {
476
+ return this.props
477
+ .map((prop) => {
478
+ const propName = [prefix, prop.name].filter((v) => v !== "").join(".");
479
+ if (propName === prefix) {
480
+ return null;
481
+ }
482
+ if (isRelationProp(prop)) {
483
+ if (maxDepth < 0) {
484
+ return null;
485
+ }
486
+ if (froms.includes(prop.with)) {
487
+ // 역방향 relation인 경우 제외
488
+ return null;
489
+ }
490
+ // 정방향 relation인 경우 recursive 콜
491
+ const relMd = EntityManager.get(prop.with);
492
+ return relMd.getFieldExprs(propName, maxDepth - 1, [
493
+ ...froms,
494
+ this.id,
495
+ ]);
496
+ }
497
+ return propName;
498
+ })
499
+ .flat()
500
+ .filter((f) => f !== null) as string[];
501
+ }
502
+
503
+ getTableColumns(): string[] {
504
+ return this.props.map((prop) => prop.name);
505
+ }
506
+
507
+ registerModulePaths() {
508
+ const basePath = `${this.names.parentFs}`;
509
+
510
+ // base-scheme
511
+ EntityManager.setModulePath(
512
+ `${this.id}BaseSchema`,
513
+ `${basePath}/${this.names.parentFs}.generated`
514
+ );
515
+
516
+ // subset
517
+ if (Object.keys(this.subsets).length > 0) {
518
+ EntityManager.setModulePath(
519
+ `${this.id}SubsetKey`,
520
+ `${basePath}/${this.names.parentFs}.generated`
521
+ );
522
+ EntityManager.setModulePath(
523
+ `${this.id}SubsetMapping`,
524
+ `${basePath}/${this.names.parentFs}.generated`
525
+ );
526
+ Object.keys(this.subsets).map((subsetKey) => {
527
+ EntityManager.setModulePath(
528
+ `${this.id}Subset${subsetKey.toUpperCase()}`,
529
+ `${basePath}/${this.names.parentFs}.generated`
530
+ );
531
+ });
532
+ }
533
+
534
+ // types
535
+ const typesModulePath = `${basePath}/${this.names.parentFs}.types`;
536
+ const typesFileDistPath = path.join(
537
+ Sonamu.apiRootPath,
538
+ `dist/application/${typesModulePath}.js`
539
+ );
540
+
541
+ if (existsSync(typesFileDistPath)) {
542
+ const importPath = path.relative(__dirname, typesFileDistPath);
543
+ import(importPath).then((t) => {
544
+ this.types = Object.keys(t).reduce((result, key) => {
545
+ EntityManager.setModulePath(key, typesModulePath);
546
+ return {
547
+ ...result,
548
+ [key]: t[key],
549
+ };
550
+ }, {});
551
+ });
552
+ }
553
+
554
+ // enums
555
+ Object.keys(this.enumLabels).map((enumId) => {
556
+ EntityManager.setModulePath(
557
+ enumId,
558
+ `${basePath}/${this.names.parentFs}.generated`
559
+ );
560
+ });
561
+ }
562
+
563
+ registerTableSpecs(): void {
564
+ const uniqueColumns = uniq(
565
+ this.indexes
566
+ .filter((idx) => idx.type === "unique")
567
+ .map((idx) => idx.columns)
568
+ .flat()
569
+ );
570
+
571
+ EntityManager.setTableSpec({
572
+ name: this.table,
573
+ uniqueColumns,
574
+ });
575
+ }
576
+
577
+ toJson(): EntityJson {
578
+ return {
579
+ id: this.id,
580
+ parentId: this.parentId,
581
+ table: this.table,
582
+ title: this.title,
583
+ props: this.props,
584
+ indexes: this.indexes,
585
+ subsets: this.subsets,
586
+ enums: this.enumLabels,
587
+ };
588
+ }
589
+
590
+ async save(): Promise<void> {
591
+ const jsonPath = path.join(
592
+ Sonamu.apiRootPath,
593
+ `src/application/${this.names.parentFs}/${this.names.fs}.entity.json`
594
+ );
595
+ const json = this.toJson();
596
+ writeFileSync(
597
+ jsonPath,
598
+ prettier.format(JSON.stringify(json), {
599
+ parser: "json",
600
+ })
601
+ );
602
+
603
+ // reload
604
+ await EntityManager.register(json);
605
+ }
606
+
607
+ getSubsetRows(
608
+ _subsets?: { [key: string]: string[] },
609
+ prefixes: string[] = []
610
+ ): EntitySubsetRow[] {
611
+ if (prefixes.length > 10) {
612
+ return [];
613
+ }
614
+
615
+ const subsets = _subsets ?? this.subsets;
616
+ const subsetKeys = Object.keys(subsets);
617
+ const allFields = uniq(subsetKeys.map((key) => subsets[key]).flat());
618
+
619
+ return this.props.map((prop) => {
620
+ if (
621
+ prop.type === "relation" &&
622
+ allFields.find((f) =>
623
+ f.startsWith([...prefixes, prop.name].join(".") + ".")
624
+ )
625
+ ) {
626
+ const relEntity = EntityManager.get(prop.with);
627
+ const children = relEntity.getSubsetRows(subsets, [
628
+ ...prefixes,
629
+ `${prop.name}`,
630
+ ]);
631
+
632
+ return {
633
+ field: prop.name,
634
+ children,
635
+ relationEntity: prop.with,
636
+ prefixes,
637
+ isOpen: children.length > 0,
638
+ has: Object.fromEntries(
639
+ subsetKeys.map((subsetKey) => {
640
+ return [
641
+ subsetKey,
642
+ children.every((child) => child.has[subsetKey] === true),
643
+ ];
644
+ })
645
+ ),
646
+ };
647
+ }
648
+
649
+ return {
650
+ field: prop.name,
651
+ children: [],
652
+ relationEntity: prop.type === "relation" ? prop.with : undefined,
653
+ prefixes,
654
+ has: Object.fromEntries(
655
+ subsetKeys.map((subsetKey) => {
656
+ const subsetFields = subsets[subsetKey];
657
+ const has = subsetFields.some((f) =>
658
+ f.startsWith([...prefixes, prop.name].join("."))
659
+ );
660
+ return [subsetKey, has];
661
+ })
662
+ ),
663
+ };
664
+ });
665
+ }
666
+
667
+ async createProp(prop: EntityProp, at?: number): Promise<void> {
668
+ if (!at) {
669
+ this.props.push(prop);
670
+ } else {
671
+ this.props.splice(at, 0, prop);
672
+ }
673
+ await this.save();
674
+ }
675
+
676
+ async modifyProp(newProp: EntityProp, at: number): Promise<void> {
677
+ // 프롭 수정
678
+ const oldName = this.props[at].name;
679
+ this.props[at] = newProp;
680
+
681
+ // 저장할 엔티티
682
+ const entities: Entity[] = [this];
683
+
684
+ // 이름이 바뀐 경우
685
+ if (oldName !== newProp.name) {
686
+ // 전체 엔티티에서 현재 수정된 프롭을 참조하고 있는 모든 서브셋필드 찾아서 수정
687
+ const allEntityIds = EntityManager.getAllIds();
688
+ for (const relEntityId of allEntityIds) {
689
+ const relEntity = EntityManager.get(relEntityId);
690
+ const relEntitySubsetKeys = Object.keys(relEntity.subsets);
691
+ for (const subsetKey of relEntitySubsetKeys) {
692
+ 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
704
+ );
705
+ entities.push(relEntity);
706
+ }
707
+ }
708
+ }
709
+ }
710
+
711
+ await Promise.all(entities.map(async (entity) => entity.save()));
712
+ }
713
+
714
+ async delProp(at: number): Promise<void> {
715
+ const oldName = this.props[at].name;
716
+ this.props.splice(at, 1);
717
+
718
+ // 저장할 엔티티
719
+ const entities: Entity[] = [this];
720
+
721
+ // 전체 엔티티에서 현재 삭제된 프롭을 참조하고 있는 모든 서브셋필드 찾아서 제외
722
+ const allEntityIds = EntityManager.getAllIds();
723
+ for (const relEntityId of allEntityIds) {
724
+ const relEntity = EntityManager.get(relEntityId);
725
+ const relEntitySubsetKeys = Object.keys(relEntity.subsets);
726
+ for (const subsetKey of relEntitySubsetKeys) {
727
+ 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
+ );
737
+ entities.push(relEntity);
738
+ }
739
+ }
740
+ }
741
+
742
+ await Promise.all(entities.map(async (entity) => entity.save()));
743
+ }
744
+
745
+ getEntityIdFromSubsetField(subsetField: string): string {
746
+ if (subsetField.includes(".") === false) {
747
+ return this.id;
748
+ }
749
+
750
+ // 서브셋 필드의 마지막은 프롭이므로 제외
751
+ const arr = subsetField.split(".").slice(0, -1);
752
+
753
+ // 서브셋 필드를 내려가면서 마지막으로 relation된 엔티티를 찾음
754
+ const lastEntity = arr.reduce((entity, field) => {
755
+ const relProp = entity.props.find((p) => p.name === field);
756
+ if (!relProp || relProp.type !== "relation") {
757
+ console.debug({ arr, thisId: this.id });
758
+ throw new Error(`잘못된 서브셋키 ${subsetField}`);
759
+ }
760
+ return EntityManager.get(relProp.with);
761
+ }, this as Entity);
762
+ return lastEntity.id;
763
+ }
764
+
765
+ async moveProp(at: number, to: number): Promise<void> {
766
+ const prop = this.props[at];
767
+ const newProps = [...this.props];
768
+ newProps.splice(to, 0, prop);
769
+ newProps.splice(at < to ? at : at + 1, 1);
770
+ this.props = newProps;
771
+
772
+ await this.save();
773
+ }
774
+ }