sonamu 0.0.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 (60) hide show
  1. package/.pnp.cjs +15552 -0
  2. package/.pnp.loader.mjs +285 -0
  3. package/.vscode/extensions.json +6 -0
  4. package/.vscode/settings.json +9 -0
  5. package/.yarnrc.yml +5 -0
  6. package/dist/bin/cli.d.ts +2 -0
  7. package/dist/bin/cli.d.ts.map +1 -0
  8. package/dist/bin/cli.js +123 -0
  9. package/dist/bin/cli.js.map +1 -0
  10. package/dist/index.js +34 -0
  11. package/package.json +60 -0
  12. package/src/api/caster.ts +72 -0
  13. package/src/api/code-converters.ts +552 -0
  14. package/src/api/context.ts +20 -0
  15. package/src/api/decorators.ts +63 -0
  16. package/src/api/index.ts +5 -0
  17. package/src/api/init.ts +128 -0
  18. package/src/bin/cli.ts +115 -0
  19. package/src/database/base-model.ts +287 -0
  20. package/src/database/db.ts +95 -0
  21. package/src/database/knex-plugins/knex-on-duplicate-update.ts +41 -0
  22. package/src/database/upsert-builder.ts +231 -0
  23. package/src/exceptions/error-handler.ts +29 -0
  24. package/src/exceptions/so-exceptions.ts +91 -0
  25. package/src/index.ts +17 -0
  26. package/src/shared/web.shared.ts.txt +119 -0
  27. package/src/smd/migrator.ts +1462 -0
  28. package/src/smd/smd-manager.ts +141 -0
  29. package/src/smd/smd-utils.ts +266 -0
  30. package/src/smd/smd.ts +533 -0
  31. package/src/syncer/index.ts +1 -0
  32. package/src/syncer/syncer.ts +1283 -0
  33. package/src/templates/base-template.ts +19 -0
  34. package/src/templates/generated.template.ts +247 -0
  35. package/src/templates/generated_http.template.ts +114 -0
  36. package/src/templates/index.ts +1 -0
  37. package/src/templates/init_enums.template.ts +71 -0
  38. package/src/templates/init_generated.template.ts +44 -0
  39. package/src/templates/init_types.template.ts +38 -0
  40. package/src/templates/model.template.ts +168 -0
  41. package/src/templates/model_test.template.ts +39 -0
  42. package/src/templates/service.template.ts +263 -0
  43. package/src/templates/smd.template.ts +49 -0
  44. package/src/templates/view_enums_buttonset.template.ts +34 -0
  45. package/src/templates/view_enums_dropdown.template.ts +67 -0
  46. package/src/templates/view_enums_select.template.ts +60 -0
  47. package/src/templates/view_form.template.ts +397 -0
  48. package/src/templates/view_id_all_select.template.ts +34 -0
  49. package/src/templates/view_id_async_select.template.ts +113 -0
  50. package/src/templates/view_list.template.ts +652 -0
  51. package/src/templates/view_list_columns.template.ts +59 -0
  52. package/src/templates/view_search_input.template.ts +67 -0
  53. package/src/testing/fixture-manager.ts +271 -0
  54. package/src/types/types.ts +668 -0
  55. package/src/typings/knex.d.ts +24 -0
  56. package/src/utils/controller.ts +21 -0
  57. package/src/utils/lodash-able.ts +11 -0
  58. package/src/utils/model.ts +33 -0
  59. package/src/utils/utils.ts +28 -0
  60. package/tsconfig.json +47 -0
@@ -0,0 +1,397 @@
1
+ import { camelize } from "inflection";
2
+ import { z } from "zod";
3
+ import { RenderingNode, TemplateKey, TemplateOptions } from "../types/types";
4
+ import { SMDManager, SMDNamesRecord } from "../smd/smd-manager";
5
+ import { RenderedTemplate } from "../syncer/syncer";
6
+ import { Template } from "./base-template";
7
+ import {
8
+ getEnumInfoFromColName,
9
+ getRelationPropFromColName,
10
+ } from "./view_list.template";
11
+
12
+ export class Template__view_form extends Template {
13
+ constructor() {
14
+ super("view_form");
15
+ }
16
+
17
+ getTargetAndPath(names: SMDNamesRecord) {
18
+ return {
19
+ target: "web/src/pages/admin",
20
+ path: `${names.fsPlural}/form.tsx`,
21
+ };
22
+ }
23
+
24
+ wrapFC(body: string, label?: string): string {
25
+ return [
26
+ `<Form.Field>${label ? `\n <label>${label}</label>` : ""}`,
27
+ body,
28
+ `</Form.Field>`,
29
+ ].join("\n");
30
+ }
31
+ wrapFG(body: string, label?: string): string {
32
+ return [
33
+ `<Form.Group widths="equal">`,
34
+ this.wrapFC(body, label),
35
+ `</Form.Group>`,
36
+ ].join("\n");
37
+ }
38
+
39
+ renderColumnImport(smdId: string, col: RenderingNode) {
40
+ if (col.renderType === "enums") {
41
+ const { id, targetMDNames } = getEnumInfoFromColName(smdId, col.name);
42
+ const componentId = `${id}Select`;
43
+ return `import { ${componentId} } from "src/components/${targetMDNames.fs}/${componentId}";`;
44
+ } else if (col.renderType === "number-fk_id") {
45
+ const relProp = getRelationPropFromColName(
46
+ smdId,
47
+ col.name.replace("_id", "")
48
+ );
49
+ const targetNames = SMDManager.getNamesFromId(relProp.with);
50
+ const componentId = `${relProp.with}IdAsyncSelect`;
51
+ return `import { ${componentId} } from "src/components/${targetNames.fs}/${componentId}";`;
52
+ } else {
53
+ throw new Error(`렌더 불가능한 임포트 ${col.name} ${col.renderType}`);
54
+ }
55
+ }
56
+
57
+ renderColumn(
58
+ smdId: string,
59
+ col: RenderingNode,
60
+ names: SMDNamesRecord,
61
+ parent: string = ""
62
+ ): string {
63
+ let regExpr: string = "";
64
+ regExpr = `{...register(\`${parent}${col.name}\`)}`;
65
+
66
+ switch (col.renderType) {
67
+ case "string-plain":
68
+ if (
69
+ col.zodType instanceof z.ZodString &&
70
+ (col.zodType.maxLength ?? 0) <= 512
71
+ ) {
72
+ return `<Input placeholder="${col.label}" ${regExpr} />`;
73
+ } else {
74
+ return `<TextArea rows={8} placeholder="${col.label}" ${regExpr} />`;
75
+ }
76
+ case "string-datetime":
77
+ return `<SQLDateTimeInput ${regExpr} />`;
78
+ case "number-id":
79
+ return `<input type="hidden" ${regExpr} />`;
80
+ case "number-plain":
81
+ return `<NumberInput placeholder="${col.label}" ${regExpr} />`;
82
+ case "boolean":
83
+ return `<BooleanToggle ${regExpr} />`;
84
+ case "string-image":
85
+ return `<ImageUploader multiple={false} ${regExpr} />`;
86
+ case "array-images":
87
+ return `<ImageUploader multiple={true} ${regExpr} maxSize={5} />`;
88
+ case "enums":
89
+ try {
90
+ let enumId: string;
91
+ if (col.name === "orderBy") {
92
+ enumId = `${names.capital}${camelize(col.name)}Select`;
93
+ } else {
94
+ const { id } = getEnumInfoFromColName(smdId, col.name);
95
+ enumId = `${id}Select`;
96
+ }
97
+ return `<${enumId} ${regExpr} ${
98
+ col.optional || col.nullable ? "clearable" : ""
99
+ } />`;
100
+ } catch {
101
+ return `<>찾을 수 없는 Enum ${col.name}</>`;
102
+ }
103
+ case "number-fk_id":
104
+ try {
105
+ const relProp = getRelationPropFromColName(
106
+ smdId,
107
+ col.name.replace("_id", "")
108
+ );
109
+ const fkId = `${relProp.with}IdAsyncSelect`;
110
+ return `<${fkId} {...register('${col.name}')} ${
111
+ col.optional || col.nullable ? "clearable" : ""
112
+ } subset="A" />`;
113
+ } catch {
114
+ return `<>${col.name} 찾을 수 없음</>`;
115
+ }
116
+ case "array":
117
+ return `{form.${col.name}.map((elem, index) => ${this.renderColumn(
118
+ smdId,
119
+ col.element!,
120
+ names,
121
+ `${parent}${col.name}[\${index}]`
122
+ )})}`;
123
+ case "object":
124
+ return (
125
+ `<Form.Group className="${col.name}"${
126
+ parent !== "" ? " key={index}" : ""
127
+ }>` +
128
+ col
129
+ .children!.map((child) =>
130
+ this.wrapFC(
131
+ this.renderColumn(smdId, child, names, `${parent}.`),
132
+ child.label
133
+ )
134
+ )
135
+ .join("\n") +
136
+ "</Form.Group>"
137
+ );
138
+ default:
139
+ throw new Error(
140
+ `대응 불가능한 렌더 타입 ${col.renderType} on ${col.name}`
141
+ );
142
+ }
143
+ }
144
+
145
+ resolveDefaultValue(columns: RenderingNode[]): object {
146
+ return columns.reduce((result, col) => {
147
+ if (col.optional) {
148
+ return result;
149
+ }
150
+
151
+ let value: unknown;
152
+ if (col.nullable === true) {
153
+ value = null;
154
+ } else if (col.zodType instanceof z.ZodNumber) {
155
+ value = 0;
156
+ } else if (col.zodType instanceof z.ZodEnum) {
157
+ value = Object.keys(col.zodType.Enum)[0];
158
+ } else if (col.zodType instanceof z.ZodBoolean) {
159
+ value = false;
160
+ } else if (col.zodType instanceof z.ZodString) {
161
+ if (col.renderType === "string-datetime") {
162
+ value = "now()";
163
+ } else {
164
+ value = "";
165
+ }
166
+ } else if (col.zodType instanceof z.ZodArray) {
167
+ value = [];
168
+ } else if (col.zodType instanceof z.ZodObject) {
169
+ value = {};
170
+ }
171
+
172
+ result[col.name] = value;
173
+ return result;
174
+ }, {} as { [key: string]: unknown });
175
+ }
176
+
177
+ render(
178
+ { smdId }: TemplateOptions["view_form"],
179
+ saveParamsNode: RenderingNode
180
+ ) {
181
+ const names = SMDManager.getNamesFromId(smdId);
182
+ const columns = (saveParamsNode.children as RenderingNode[]).filter(
183
+ (col) => col.name !== "id"
184
+ );
185
+
186
+ const defaultValue = this.resolveDefaultValue(columns);
187
+
188
+ // 프리 템플릿
189
+ const preTemplates: RenderedTemplate["preTemplates"] = (
190
+ columns as RenderingNode[]
191
+ )
192
+ .filter((col) => {
193
+ if (
194
+ col.name !== "id" &&
195
+ (["enums", "number-id"].includes(col.renderType) ||
196
+ col.name.endsWith("_id"))
197
+ ) {
198
+ try {
199
+ getRelationPropFromColName(smdId, col.name.replace("_id", ""));
200
+ } catch {
201
+ return false;
202
+ }
203
+ return true;
204
+ } else {
205
+ return false;
206
+ }
207
+ })
208
+ .map((col) => {
209
+ let key: TemplateKey;
210
+ let targetMdId = smdId;
211
+ if (col.renderType === "enums") {
212
+ key = "view_enums_select";
213
+ } else {
214
+ key = "view_id_async_select";
215
+ const relProp = getRelationPropFromColName(
216
+ smdId,
217
+ col.name.replace("_id", "")
218
+ );
219
+ targetMdId = relProp.with;
220
+ }
221
+
222
+ return {
223
+ key: key as TemplateKey,
224
+ options: {
225
+ smdId: targetMdId,
226
+ node: col,
227
+ },
228
+ };
229
+ })
230
+ .filter((preTemplate) => {
231
+ if (preTemplate.key === "view_id_async_select") {
232
+ try {
233
+ SMDManager.get(preTemplate.options.smdId);
234
+ return true;
235
+ } catch {
236
+ return false;
237
+ }
238
+ }
239
+ return true;
240
+ });
241
+
242
+ return {
243
+ ...this.getTargetAndPath(names),
244
+ body: `
245
+ import React, { useEffect, useState, Dispatch, SetStateAction } from 'react';
246
+ import { useSearchParams } from 'react-router-dom';
247
+ import {
248
+ Button,
249
+ Checkbox,
250
+ Form,
251
+ Header,
252
+ Input,
253
+ Segment,
254
+ TextArea,
255
+ Label,
256
+ } from 'semantic-ui-react';
257
+ import { DateTime } from "luxon";
258
+
259
+ import { BackLink } from 'src/typeframe/components/BackLink';
260
+ import { LinkInput } from 'src/typeframe/components/LinkInput';
261
+ import { ImageUploader } from 'src/typeframe/components/ImageUploader';
262
+ import { NumberInput } from 'src/typeframe/components/NumberInput';
263
+ import { BooleanToggle } from 'src/typeframe/components/BooleanToggle';
264
+ import { SQLDateTimeInput } from "src/typeframe/components/SQLDateTimeInput";
265
+ import { defCatch } from 'src/typeframe/fetch';
266
+
267
+ import { ${names.capital}SaveParams } from 'src/services/${names.fs}/${
268
+ names.fs
269
+ }.types';
270
+ import { useTypeForm, useGoBack } from 'src/typeframe/helpers';
271
+ import { usePubSub } from 'src/typeframe/pubsub';
272
+ import { ${names.capital}Service } from 'src/services/${names.fs}/${
273
+ names.fs
274
+ }.service';
275
+ import { ${names.capital}SubsetA } from 'src/services/${names.fs}/${
276
+ names.fs
277
+ }.generated';
278
+ ${columns
279
+ .filter((col) => ["number-fk_id", "enums"].includes(col.renderType))
280
+ .map((col) => {
281
+ return this.renderColumnImport(smdId, col);
282
+ })
283
+ .join("\n")}
284
+
285
+ export default function ${names.capitalPlural}FormPage() {
286
+ // 라우팅 searchParams
287
+ const [searchParams] = useSearchParams();
288
+ const query = {
289
+ id: searchParams.get('id') ?? undefined,
290
+ };
291
+
292
+ return <${
293
+ names.capitalPlural
294
+ }Form id={query?.id ? Number(query.id) : undefined} />;
295
+ }
296
+ type ${names.capitalPlural}FormProps = {
297
+ id?: number;
298
+ mode?: 'page' | 'modal';
299
+ };
300
+ export function ${names.capitalPlural}Form({ id, mode }: ${
301
+ names.capitalPlural
302
+ }FormProps) {
303
+ // 편집시 기존 row
304
+ const [row, setRow] = useState<${names.capital}SubsetA | undefined>();
305
+
306
+ // ${names.capital}SaveParams 폼
307
+ const { form, setForm, register } = useTypeForm(${
308
+ names.capital
309
+ }SaveParams, ${JSON.stringify(defaultValue).replace(
310
+ '"now()"',
311
+ "DateTime.local().toSQL().slice(0, 19)"
312
+ )});
313
+
314
+ // 수정일 때 기존 row 콜
315
+ useEffect(() => {
316
+ if (id) {
317
+ ${names.capital}Service.get${names.capital}('A', id).then((row) => {
318
+ setRow(row);
319
+ setForm({
320
+ ...row,
321
+ ${columns
322
+ .filter((col) => col.renderType === "number-fk_id")
323
+ .map((col) => {
324
+ if (col.nullable) {
325
+ return `${col.name}: row.${col.name.replace(
326
+ "_id",
327
+ "?.id"
328
+ )} ?? null`;
329
+ } else {
330
+ return `${col.name}: row.${col.name.replace("_id", ".id")}`;
331
+ }
332
+ })
333
+ .join(",\n")}
334
+ });
335
+ });
336
+ }
337
+ }, [id]);
338
+
339
+ // 저장
340
+ const { goBack } = useGoBack();
341
+ const handleSubmit = () => {
342
+ ${names.capital}Service.save([form]).then(([id]) => {
343
+ if (mode !== 'modal') {
344
+ goBack('/admin/${names.fsPlural}');
345
+ }
346
+ }).catch(defCatch);
347
+ };
348
+
349
+ // 모달 서브밋 핸들링
350
+ const { subscribe } = usePubSub();
351
+ useEffect(() => {
352
+ if (id) {
353
+ return subscribe(\`${names.fs}#\${id}.submitted\`, () => {
354
+ handleSubmit();
355
+ });
356
+ }
357
+ }, [form]);
358
+
359
+ return (
360
+ <div className="form">
361
+ <Segment padded basic>
362
+ <Segment padded color="grey">
363
+ <div className="header-row">
364
+ <Header>
365
+ ${names.capital}{id ? \`#\${id} 수정 폼\` : ' 작성 폼'}
366
+ </Header>
367
+ <div className="buttons">
368
+ <BackLink primary size="tiny" to="/admin/${names.fsPlural}">
369
+ 목록
370
+ </BackLink>
371
+ </div>
372
+ </div>
373
+ <Form>
374
+ ${columns
375
+ .map((col) =>
376
+ this.wrapFG(this.renderColumn(smdId, col, names), col.label)
377
+ )
378
+ .join("\n")}
379
+ {mode !== 'modal' && (
380
+ <Segment basic textAlign="center">
381
+ <Button type="submit" primary onClick={handleSubmit}>
382
+ 저장
383
+ </Button>
384
+ </Segment>
385
+ )}
386
+ </Form>
387
+ </Segment>
388
+ </Segment>
389
+ </div>
390
+ );
391
+ }
392
+ `.trim(),
393
+ importKeys: [],
394
+ preTemplates,
395
+ };
396
+ }
397
+ }
@@ -0,0 +1,34 @@
1
+ import { TemplateOptions } from "../types/types";
2
+ import { SMDManager, SMDNamesRecord } from "../smd/smd-manager";
3
+ import { Template } from "./base-template";
4
+
5
+ export class Template__view_id_all_select extends Template {
6
+ constructor() {
7
+ super("view_id_all_select");
8
+ }
9
+
10
+ getTargetAndPath(names: SMDNamesRecord) {
11
+ return {
12
+ target: "web/src/components",
13
+ path: `${names.fs}/${names.capital}IdAllSelect.tsx`,
14
+ };
15
+ }
16
+
17
+ render({ smdId }: TemplateOptions["view_id_all_select"]) {
18
+ const names = SMDManager.getNamesFromId(smdId);
19
+
20
+ return {
21
+ ...this.getTargetAndPath(names),
22
+ body: `
23
+ /*
24
+ view_id_all_select
25
+ ${JSON.stringify({
26
+ key: this.key,
27
+ options: smdId,
28
+ })}
29
+ */
30
+ `.trim(),
31
+ importKeys: [],
32
+ };
33
+ }
34
+ }
@@ -0,0 +1,113 @@
1
+ import { TemplateOptions } from "../types/types";
2
+ import { SMDManager, SMDNamesRecord } from "../smd/smd-manager";
3
+ import { Template } from "./base-template";
4
+
5
+ export class Template__view_id_async_select extends Template {
6
+ constructor() {
7
+ super("view_id_async_select");
8
+ }
9
+
10
+ getTargetAndPath(names: SMDNamesRecord) {
11
+ return {
12
+ target: "web/src/components",
13
+ path: `${names.fs}/${names.capital}IdAsyncSelect.tsx`,
14
+ };
15
+ }
16
+
17
+ render({ smdId, textField }: TemplateOptions["view_id_async_select"]) {
18
+ const names = SMDManager.getNamesFromId(smdId);
19
+
20
+ const smd = SMDManager.get(smdId);
21
+ if (!textField) {
22
+ const pickedProp = smd.props.find((prop) =>
23
+ ["name", "title"].includes(prop.name)
24
+ );
25
+ if (pickedProp) {
26
+ textField = pickedProp.name;
27
+ } else {
28
+ const candidateProp = smd.props.find((prop) => prop.type === "string");
29
+ if (candidateProp) {
30
+ textField = candidateProp.name;
31
+ } else {
32
+ console.log("textField 찾을 수 없음");
33
+ }
34
+ }
35
+ }
36
+
37
+ return {
38
+ ...this.getTargetAndPath(names),
39
+ body: `
40
+ import React, { useState, useEffect, SyntheticEvent } from "react";
41
+ import { DropdownProps, DropdownItemProps, DropdownOnSearchChangeData, Dropdown } from "semantic-ui-react";
42
+ import { ${names.capital}SubsetKey, ${
43
+ names.capital
44
+ }SubsetMapping } from "src/services/${names.fs}/${names.fs}.generated";
45
+ import { ${names.capital}Service } from "src/services/${names.fs}/${
46
+ names.fs
47
+ }.service";
48
+ import { ${names.capital}ListParams } from "src/services/${names.fs}/${
49
+ names.fs
50
+ }.types";
51
+
52
+ export function ${names.capital}IdAsyncSelect<T extends ${
53
+ names.capital
54
+ }SubsetKey>(
55
+ { subset, baseListParams, textField, valueField, ...props }: DropdownProps & {
56
+ subset: T;
57
+ baseListParams?: ${names.capital}ListParams;
58
+ textField${textField ? "?" : ""}: keyof ${names.capital}SubsetMapping[T];
59
+ valueField?: keyof ${names.capital}SubsetMapping[T];
60
+ },
61
+ ) {
62
+ const [options, setOptions] = useState<DropdownItemProps[]>([]);
63
+ const [listParams, setListParams] = useState<${names.capital}ListParams>(
64
+ baseListParams ?? {},
65
+ );
66
+
67
+ const { data, error } = ${names.capital}Service.use${
68
+ names.capitalPlural
69
+ }(subset, listParams);
70
+ const { rows: ${names.camelPlural}, total } = data ?? {};
71
+
72
+ useEffect(() => {
73
+ setOptions(
74
+ (${names.camelPlural} ?? []).map((${names.camel}) => {
75
+ return {
76
+ key: ${names.camel}.id,
77
+ value: ${names.camel}[valueField ?? 'id'] as string | number,
78
+ text: String(${names.camel}[textField${
79
+ textField ? ` ?? '${textField}'` : ""
80
+ }]),
81
+ };
82
+ }),
83
+ );
84
+ }, [${names.camelPlural}]);
85
+
86
+ const handleSearchChange = (
87
+ e: SyntheticEvent<HTMLElement, Event>,
88
+ data: DropdownOnSearchChangeData,
89
+ ) => {
90
+ setListParams({
91
+ ...listParams,
92
+ keyword: data.searchQuery,
93
+ });
94
+ };
95
+
96
+ return (
97
+ <Dropdown
98
+ placeholder="${names.constant}"
99
+ selection
100
+ options={options}
101
+ onSearchChange={handleSearchChange}
102
+ disabled={!${names.camelPlural}}
103
+ loading={!${names.camelPlural}}
104
+ selectOnBlur={false}
105
+ {...props}
106
+ />
107
+ );
108
+ }
109
+ `.trim(),
110
+ importKeys: [],
111
+ };
112
+ }
113
+ }