sonamu 0.7.15 → 0.7.17

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 (96) hide show
  1. package/dist/ai/providers/rtzr/error.d.ts +1 -1
  2. package/dist/ai/providers/rtzr/error.d.ts.map +1 -1
  3. package/dist/api/config.d.ts +1 -0
  4. package/dist/api/config.d.ts.map +1 -1
  5. package/dist/api/config.js +1 -1
  6. package/dist/api/decorators.d.ts +1 -1
  7. package/dist/api/decorators.d.ts.map +1 -1
  8. package/dist/api/decorators.js +1 -1
  9. package/dist/api/sonamu.d.ts +3 -1
  10. package/dist/api/sonamu.d.ts.map +1 -1
  11. package/dist/api/sonamu.js +51 -40
  12. package/dist/database/base-model.d.ts +16 -6
  13. package/dist/database/base-model.d.ts.map +1 -1
  14. package/dist/database/base-model.js +44 -3
  15. package/dist/database/base-model.types.d.ts +29 -48
  16. package/dist/database/base-model.types.d.ts.map +1 -1
  17. package/dist/database/base-model.types.js +12 -2
  18. package/dist/database/puri.d.ts +2 -1
  19. package/dist/database/puri.d.ts.map +1 -1
  20. package/dist/database/puri.js +2 -1
  21. package/dist/database/puri.types.d.ts +3 -3
  22. package/dist/database/puri.types.d.ts.map +1 -1
  23. package/dist/database/puri.types.js +1 -1
  24. package/dist/entity/entity-manager.d.ts +8 -4
  25. package/dist/entity/entity-manager.d.ts.map +1 -1
  26. package/dist/entity/entity.d.ts +10 -1
  27. package/dist/entity/entity.d.ts.map +1 -1
  28. package/dist/entity/entity.js +84 -39
  29. package/dist/index.d.ts +1 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +2 -1
  32. package/dist/syncer/checksum.d.ts +8 -3
  33. package/dist/syncer/checksum.d.ts.map +1 -1
  34. package/dist/syncer/checksum.js +17 -9
  35. package/dist/syncer/code-generator.js +7 -2
  36. package/dist/syncer/syncer.d.ts +6 -6
  37. package/dist/syncer/syncer.d.ts.map +1 -1
  38. package/dist/syncer/syncer.js +27 -13
  39. package/dist/tasks/workflow-manager.d.ts +3 -3
  40. package/dist/tasks/workflow-manager.d.ts.map +1 -1
  41. package/dist/tasks/workflow-manager.js +15 -11
  42. package/dist/template/implementations/generated.template.d.ts.map +1 -1
  43. package/dist/template/implementations/generated.template.js +8 -6
  44. package/dist/template/implementations/model.template.js +5 -5
  45. package/dist/template/implementations/services.template.d.ts +17 -0
  46. package/dist/template/implementations/services.template.d.ts.map +1 -0
  47. package/dist/template/implementations/services.template.js +159 -0
  48. package/dist/template/implementations/view_form.template.js +2 -2
  49. package/dist/template/implementations/view_id_async_select.template.js +2 -2
  50. package/dist/template/implementations/view_list.template.js +5 -5
  51. package/dist/types/types.d.ts +43 -25
  52. package/dist/types/types.d.ts.map +1 -1
  53. package/dist/types/types.js +29 -17
  54. package/dist/ui/ai-api.d.ts +2 -0
  55. package/dist/ui/ai-api.d.ts.map +1 -1
  56. package/dist/ui/ai-api.js +43 -49
  57. package/dist/ui/ai-client.d.ts +10 -0
  58. package/dist/ui/ai-client.d.ts.map +1 -1
  59. package/dist/ui/ai-client.js +457 -437
  60. package/dist/ui/api.d.ts.map +1 -1
  61. package/dist/ui/api.js +14 -3
  62. package/dist/ui-web/assets/{index-J9MCfjCd.js → index-DzqUrTB-.js} +56 -59
  63. package/dist/ui-web/index.html +1 -1
  64. package/package.json +12 -8
  65. package/src/api/config.ts +3 -0
  66. package/src/api/decorators.ts +6 -1
  67. package/src/api/sonamu.ts +71 -52
  68. package/src/database/base-model.ts +66 -11
  69. package/src/database/base-model.types.ts +79 -76
  70. package/src/database/puri.ts +5 -1
  71. package/src/database/puri.types.ts +3 -6
  72. package/src/entity/entity.ts +83 -34
  73. package/src/index.ts +1 -0
  74. package/src/shared/app.shared.ts.txt +1 -1
  75. package/src/shared/web.shared.ts.txt +0 -43
  76. package/src/syncer/checksum.ts +31 -9
  77. package/src/syncer/code-generator.ts +8 -1
  78. package/src/syncer/syncer.ts +38 -26
  79. package/src/tasks/workflow-manager.ts +16 -12
  80. package/src/template/implementations/generated.template.ts +17 -3
  81. package/src/template/implementations/model.template.ts +4 -4
  82. package/src/template/implementations/services.template.ts +226 -0
  83. package/src/template/implementations/view_form.template.ts +1 -1
  84. package/src/template/implementations/view_id_async_select.template.ts +1 -1
  85. package/src/template/implementations/view_list.template.ts +4 -4
  86. package/src/types/types.ts +33 -16
  87. package/src/ui/ai-api.ts +61 -60
  88. package/src/ui/ai-client.ts +535 -499
  89. package/src/ui/api.ts +14 -2
  90. package/src/ui/entity.instructions.md +536 -0
  91. package/dist/template/implementations/service.template.d.ts +0 -29
  92. package/dist/template/implementations/service.template.d.ts.map +0 -1
  93. package/dist/template/implementations/service.template.js +0 -202
  94. package/dist/ui-web/assets/provider-utils_false-BKJD46kk.js +0 -1
  95. package/dist/ui-web/assets/provider-utils_false-Bu5lmX18.js +0 -1
  96. package/src/template/implementations/service.template.ts +0 -328
@@ -14,12 +14,15 @@ import {
14
14
  isBelongsToOneRelationProp,
15
15
  isEnumProp,
16
16
  isHasManyRelationProp,
17
+ isInternalSubsetField,
17
18
  isManyToManyRelationProp,
18
19
  isOneToOneRelationProp,
19
20
  isRelationProp,
21
+ isVirtualCodeProp,
20
22
  isVirtualProp,
23
+ normalizeSubsetField,
21
24
  type RelationProp,
22
- type StringProp,
25
+ type SubsetField,
23
26
  type SubsetQuery,
24
27
  } from "../types/types";
25
28
  import { importMembers } from "../utils/esm-utils";
@@ -50,6 +53,9 @@ export class Entity {
50
53
  subsets: {
51
54
  [key: string]: string[];
52
55
  };
56
+ subsetsInternal: {
57
+ [key: string]: string[];
58
+ };
53
59
  types: {
54
60
  [name: string]: z.ZodTypeAny;
55
61
  } = {};
@@ -98,8 +104,13 @@ export class Entity {
98
104
  // indexes
99
105
  this.indexes = indexes ?? [];
100
106
 
101
- // subsets
102
- this.subsets = subsets ?? {};
107
+ // subsets: SubsetField[]를 파싱하여 subsets(일반)와 subsetsInternal(internal)로 분리
108
+ this.subsets = {};
109
+ this.subsetsInternal = {};
110
+ for (const [key, fields] of Object.entries(subsets ?? {})) {
111
+ this.subsets[key] = fields.filter((f) => !isInternalSubsetField(f)).map(normalizeSubsetField);
112
+ this.subsetsInternal[key] = fields.filter(isInternalSubsetField).map(normalizeSubsetField);
113
+ }
103
114
 
104
115
  // enums
105
116
  this.enumLabels = enums ?? {};
@@ -117,11 +128,18 @@ export class Entity {
117
128
  };
118
129
  }
119
130
 
131
+ /**
132
+ * 쿼리용 서브셋 필드를 반환합니다 (subsets + subsetsInternal 합침)
133
+ */
134
+ getSubsetFieldsForQuery(subsetKey: string): string[] {
135
+ return [...(this.subsets[subsetKey] ?? []), ...(this.subsetsInternal[subsetKey] ?? [])];
136
+ }
137
+
120
138
  /**
121
139
  * 주어진 이름(subsetKey)의 subset을 실제로 가져오는 Puri 코드 구현체 string을 반환합니다.
122
140
  */
123
141
  getPuriSubsetQuery(subsetKey: string): string {
124
- const subset = this.subsets[subsetKey];
142
+ const subset = this.getSubsetFieldsForQuery(subsetKey);
125
143
  const subsetQuery = this.resolveSubsetQuery("", subset);
126
144
 
127
145
  const lines: string[] = [];
@@ -148,7 +166,6 @@ export class Entity {
148
166
 
149
167
  // select - 입체적 구조로 생성
150
168
  const selectObj = this.buildNestedSelectObject(subsetQuery.select);
151
-
152
169
  lines.push(`.select(${this.stringifyNestedSelectObject(selectObj)});`);
153
170
 
154
171
  return lines.join("\n");
@@ -263,7 +280,7 @@ export class Entity {
263
280
  }
264
281
 
265
282
  getPuriLoaderQuery(subsetKey: string): string {
266
- const subset = this.subsets[subsetKey];
283
+ const subset = this.getSubsetFieldsForQuery(subsetKey);
267
284
  const { loaders } = this.resolveSubsetQuery("", subset);
268
285
 
269
286
  const lines: string[] = [`[`];
@@ -370,7 +387,7 @@ export class Entity {
370
387
  subset SELECT/JOIN/LOADER 결과 리턴
371
388
  */
372
389
  getSubsetQuery(subsetKey: string): SubsetQuery {
373
- const subset = this.subsets[subsetKey];
390
+ const subset = this.getSubsetFieldsForQuery(subsetKey);
374
391
 
375
392
  const result: SubsetQuery = this.resolveSubsetQuery("", subset);
376
393
  return result;
@@ -404,12 +421,16 @@ export class Entity {
404
421
  // 현재 테이블 필드셋은 select, virtual에 추가하고 리턴
405
422
  if (groupKey === "") {
406
423
  const realFields = fields.filter((field) => !isVirtualProp(this.propsDict[field]));
407
- const virtualFields = fields.filter((field) => isVirtualProp(this.propsDict[field]));
424
+ // virtualType: "code" (또는 undefined) virtual prop만 r.virtual에 추가
425
+ // virtualType: "query"인 경우 사용자가 appendSelect로 직접 추가하므로 제외
426
+ const virtualCodeFields = fields.filter((field) =>
427
+ isVirtualCodeProp(this.propsDict[field]),
428
+ );
408
429
 
409
430
  if (prefix === "") {
410
431
  // 현재 테이블인 경우
411
432
  r.select = r.select.concat(realFields.map((field) => `${this.table}.${field}`));
412
- r.virtual = r.virtual.concat(virtualFields);
433
+ r.virtual = r.virtual.concat(virtualCodeFields);
413
434
  } else {
414
435
  // 넘어온 테이블인 경우
415
436
  r.select = r.select.concat(
@@ -605,25 +626,12 @@ export class Entity {
605
626
  },
606
627
  );
607
628
 
608
- return Object.keys(groups).flatMap((key) => {
629
+ return Object.keys(groups).flatMap<EntityPropNode, EntityPropNode[]>((key) => {
609
630
  const group = groups[key];
610
631
 
611
632
  // 일반 prop 처리
612
633
  if (key === "") {
613
634
  return group.map((propName) => {
614
- // FIXME: 이거 나중에 없애야함
615
- if (propName === "말도안되는프롭명__이거왜타입처리가꼬여서이러지?") {
616
- return {
617
- nodeType: "plain" as const,
618
- prop: {
619
- type: "string",
620
- name: "uuid",
621
- length: 128,
622
- } as StringProp,
623
- children: [],
624
- } as EntityPropNode;
625
- }
626
-
627
635
  const prop = entity.props.find((p) => p.name === propName);
628
636
  if (prop === undefined) {
629
637
  console.log({ propName, groups });
@@ -632,7 +640,6 @@ export class Entity {
632
640
  return {
633
641
  nodeType: "plain" as const,
634
642
  prop,
635
- children: [],
636
643
  };
637
644
  });
638
645
  }
@@ -656,7 +663,6 @@ export class Entity {
656
663
  name: `${key}_id`,
657
664
  nullable: prop.nullable,
658
665
  },
659
- children: [],
660
666
  };
661
667
  }
662
668
  }
@@ -671,9 +677,9 @@ export class Entity {
671
677
  : ("array" as const);
672
678
 
673
679
  return {
680
+ nodeType,
674
681
  prop,
675
682
  children,
676
- nodeType,
677
683
  };
678
684
  });
679
685
  }
@@ -793,6 +799,17 @@ export class Entity {
793
799
  }
794
800
 
795
801
  toJson(): EntityJson {
802
+ // subsets와 subsetsInternal을 SubsetField[] 형태로 복원
803
+ const subsets: { [key: string]: SubsetField[] } = {};
804
+ for (const key of Object.keys(this.subsets)) {
805
+ const normalFields: SubsetField[] = this.subsets[key];
806
+ const internalFields: SubsetField[] = (this.subsetsInternal[key] ?? []).map((field) => ({
807
+ field,
808
+ internal: true,
809
+ }));
810
+ subsets[key] = [...normalFields, ...internalFields];
811
+ }
812
+
796
813
  return {
797
814
  id: this.id,
798
815
  parentId: this.parentId,
@@ -800,7 +817,7 @@ export class Entity {
800
817
  title: this.title,
801
818
  props: this.props,
802
819
  indexes: this.indexes,
803
- subsets: this.subsets,
820
+ subsets,
804
821
  enums: this.enumLabels,
805
822
  };
806
823
  }
@@ -810,7 +827,12 @@ export class Entity {
810
827
  const subsetRows = this.getSubsetRows();
811
828
  this.subsets = Object.fromEntries(
812
829
  Object.entries(this.subsets).map(([subsetKey]) => {
813
- return [subsetKey, this.subsetRowsToSubsetFields(subsetRows, subsetKey)];
830
+ return [subsetKey, this.subsetRowsToSubsetFields(subsetRows, subsetKey, false)];
831
+ }),
832
+ );
833
+ this.subsetsInternal = Object.fromEntries(
834
+ Object.entries(this.subsetsInternal).map(([subsetKey]) => {
835
+ return [subsetKey, this.subsetRowsToSubsetFields(subsetRows, subsetKey, true)];
814
836
  }),
815
837
  );
816
838
 
@@ -828,6 +850,7 @@ export class Entity {
828
850
 
829
851
  getSubsetRows(
830
852
  _subsets?: { [key: string]: string[] },
853
+ _subsetsInternal?: { [key: string]: string[] },
831
854
  prefixes: string[] = [],
832
855
  ): EntitySubsetRow[] {
833
856
  if (prefixes.length > 10) {
@@ -835,16 +858,23 @@ export class Entity {
835
858
  }
836
859
 
837
860
  const subsets = _subsets ?? this.subsets;
861
+ const subsetsInternal = _subsetsInternal ?? this.subsetsInternal;
838
862
  const subsetKeys = Object.keys(subsets);
839
863
  const allFields = unique(subsetKeys.flatMap((key) => subsets[key]));
864
+ // internal 필드도 allFields에 포함 (relation 탐색용)
865
+ const allInternalFields = unique(subsetKeys.flatMap((key) => subsetsInternal[key] ?? []));
866
+ const combinedFields = unique([...allFields, ...allInternalFields]);
840
867
 
841
868
  return this.props.map((prop) => {
842
869
  if (
843
870
  prop.type === "relation" &&
844
- allFields.find((f) => f.startsWith(`${[...prefixes, prop.name].join(".")}.`))
871
+ combinedFields.find((f) => f.startsWith(`${[...prefixes, prop.name].join(".")}.`))
845
872
  ) {
846
873
  const relEntity = EntityManager.get(prop.with);
847
- const children = relEntity.getSubsetRows(subsets, [...prefixes, `${prop.name}`]);
874
+ const children = relEntity.getSubsetRows(subsets, subsetsInternal, [
875
+ ...prefixes,
876
+ `${prop.name}`,
877
+ ]);
848
878
 
849
879
  return {
850
880
  field: prop.name,
@@ -857,9 +887,15 @@ export class Entity {
857
887
  return [subsetKey, children.every((child) => child.has[subsetKey] === true)];
858
888
  }),
859
889
  ),
890
+ isInternal: Object.fromEntries(
891
+ subsetKeys.map((subsetKey) => {
892
+ return [subsetKey, children.every((child) => child.isInternal[subsetKey] === true)];
893
+ }),
894
+ ),
860
895
  };
861
896
  }
862
897
 
898
+ const field = [...prefixes, prop.name].join(".");
863
899
  return {
864
900
  field: prop.name,
865
901
  children: [],
@@ -869,22 +905,35 @@ export class Entity {
869
905
  subsetKeys.map((subsetKey) => {
870
906
  const subsetFields = subsets[subsetKey];
871
907
  const has = subsetFields.some((f) => {
872
- const field = [...prefixes, prop.name].join(".");
873
908
  return f === field || f.startsWith(`${field}.`);
874
909
  });
875
910
  return [subsetKey, has];
876
911
  }),
877
912
  ),
913
+ isInternal: Object.fromEntries(
914
+ subsetKeys.map((subsetKey) => {
915
+ const internalFields = subsetsInternal[subsetKey] ?? [];
916
+ const isInternal = internalFields.some((f) => {
917
+ return f === field || f.startsWith(`${field}.`);
918
+ });
919
+ return [subsetKey, isInternal];
920
+ }),
921
+ ),
878
922
  };
879
923
  });
880
924
  }
881
925
 
882
- subsetRowsToSubsetFields(subsetRows: EntitySubsetRow[], subsetKey: string): string[] {
926
+ subsetRowsToSubsetFields(
927
+ subsetRows: EntitySubsetRow[],
928
+ subsetKey: string,
929
+ internal: boolean = false,
930
+ ): string[] {
931
+ const hasKey = internal ? "isInternal" : "has";
883
932
  return subsetRows
884
933
  .map((subsetRow) => {
885
934
  if (subsetRow.children.length > 0) {
886
- return this.subsetRowsToSubsetFields(subsetRow.children, subsetKey);
887
- } else if (subsetRow.has[subsetKey]) {
935
+ return this.subsetRowsToSubsetFields(subsetRow.children, subsetKey, internal);
936
+ } else if (subsetRow[hasKey][subsetKey]) {
888
937
  return subsetRow.prefixes.concat(subsetRow.field).join(".");
889
938
  } else {
890
939
  return null;
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ export type * from "./api/context";
4
4
  export * from "./api/decorators";
5
5
  export * from "./api/sonamu";
6
6
  export * from "./database/base-model";
7
+ export * from "./database/base-model.types";
7
8
  export * from "./database/db";
8
9
  export * from "./database/puri";
9
10
  export * from "./database/puri.types";
@@ -322,7 +322,7 @@ export function useSSEStream<T extends Record<string, any>>(
322
322
 
323
323
  // URL에 파라미터 추가 - 절대 URL로 변환
324
324
  const queryString = qs.stringify(params);
325
- const baseUrl = url.startsWith("http") ? url : `https://dev.amrintl.com${url}`;
325
+ const baseUrl = url.startsWith("http") ? url : `$[[baseUrl]]${url}`;
326
326
  const fullUrl = queryString ? `${baseUrl}?${queryString}` : baseUrl;
327
327
 
328
328
  const eventSource = new EventSource(fullUrl);
@@ -108,49 +108,6 @@ export const SonamuSemanticParams = z
108
108
  .partial();
109
109
  export type SonamuSemanticParams = z.infer<typeof SonamuSemanticParams>;
110
110
 
111
- /*
112
- SWR
113
- */
114
- export type SwrOptions = {
115
- conditional?: () => boolean;
116
- };
117
- export type SWRError = {
118
- name: string;
119
- message: string;
120
- statusCode: number;
121
- };
122
- export async function swrFetcher(args: [string, object]): Promise<any> {
123
- try {
124
- const [url, params] = args;
125
- const res = await axios.get(`${url}?${qs.stringify(params)}`);
126
- return res.data;
127
- } catch (e: any) {
128
- const error: any = new Error(e.response.data.message ?? e.response.message ?? "Unknown");
129
- error.statusCode = e.response?.data.statusCode ?? e.response.status;
130
- throw error;
131
- }
132
- }
133
- export async function swrPostFetcher(args: [string, object]): Promise<any> {
134
- try {
135
- const [url, params] = args;
136
- const res = await axios.post(url, params);
137
- return res.data;
138
- } catch (e: any) {
139
- const error: any = new Error(e.response.data.message ?? e.response.message ?? "Unknown");
140
- error.statusCode = e.response?.data.statusCode ?? e.response.status;
141
- throw error;
142
- }
143
- }
144
- export function handleConditional(
145
- args: [string, object],
146
- conditional?: () => boolean,
147
- ): [string, object] | null {
148
- if (conditional) {
149
- return conditional() ? args : null;
150
- }
151
- return args;
152
- }
153
-
154
111
  /*
155
112
  Utils
156
113
  */
@@ -48,22 +48,38 @@ export async function renewChecksums(): Promise<void> {
48
48
  await saveChecksums(calculatedChecksums);
49
49
  }
50
50
 
51
+ export type FileOrData =
52
+ | {
53
+ path: PathLike;
54
+ }
55
+ | {
56
+ data: string;
57
+ };
58
+
51
59
  /**
52
60
  * 두 파일의 내용이 같은지 체크섬으로 비교합니다.
53
61
  * 만약 파일이 둘 중 하나라도 없다면 비교 불가로 false 반환합니다.
54
- * @param one 파일 경로
55
- * @param two 파일 경로
62
+ * @param one 파일 경로 혹은 데이터
63
+ * @param two 파일 경로 혹은 데이터
56
64
  * @returns boolean
57
65
  */
58
- export async function areFilesSame(one: PathLike, two: PathLike): Promise<boolean> {
59
- if (!(await exists(one)) || !(await exists(two))) {
60
- return false;
61
- }
66
+ export async function areFilesSame(...files: FileOrData[]): Promise<boolean> {
67
+ const checksums: string[] = [];
62
68
 
63
- const oneChecksum = await getChecksumOfFile(one);
64
- const twoChecksum = await getChecksumOfFile(two);
69
+ for (const file of files) {
70
+ if ("path" in file && !(await exists(file.path))) {
71
+ return false;
72
+ }
73
+
74
+ checksums.push(
75
+ "path" in file ? await getChecksumOfFile(file.path) : getChecksumOfData(file.data),
76
+ );
77
+ }
65
78
 
66
- return oneChecksum === twoChecksum;
79
+ return checksums.every(
80
+ // 다음 체크섬과 비교, 만약 마지막 체크섬일 때는 첫 번째 체크섬과 비교
81
+ (checksum, index) => checksum === checksums[index === checksums.length - 1 ? 0 : index + 1],
82
+ );
67
83
  }
68
84
 
69
85
  async function getCurrentChecksums(): Promise<PathAndChecksum[]> {
@@ -125,6 +141,12 @@ async function saveChecksums(checksums: PathAndChecksum[]): Promise<void> {
125
141
  console.log("checksum saved", checksumFilePath);
126
142
  }
127
143
 
144
+ function getChecksumOfData(data: string): string {
145
+ const hash = crypto.createHash("sha1");
146
+ hash.update(data);
147
+ return hash.digest("hex");
148
+ }
149
+
128
150
  async function getChecksumOfFile(filePath: PathLike): Promise<string> {
129
151
  return new Promise<string>((resolve, reject) => {
130
152
  const hash = crypto.createHash("sha1");
@@ -115,7 +115,14 @@ async function resolveRenderedTemplate(
115
115
  const importDefs = importKeys
116
116
  .reduce(
117
117
  (r, importKey) => {
118
- const modulePath = EntityManager.getModulePath(importKey);
118
+ let modulePath = importKey;
119
+ try {
120
+ modulePath = EntityManager.getModulePath(importKey);
121
+ } catch (error) {
122
+ throw new Error(
123
+ `[resolveRenderedTemplate:${key}] ${importKey} 모듈 경로 찾기 실패: ${error}`,
124
+ );
125
+ }
119
126
  let importPath = modulePath;
120
127
  if (modulePath.includes("/") || modulePath.includes(".")) {
121
128
  importPath = wrapIf(path.relative(path.dirname(filePath), modulePath), (p) => [
@@ -161,6 +161,13 @@ export class Syncer {
161
161
  }
162
162
 
163
163
  async copySharedToTargets(targets: string[]): Promise<void> {
164
+ // 특정 변수 치환을 위해서 사용합니다.
165
+ const convertMap = {
166
+ baseUrl:
167
+ Sonamu.config.server.baseUrl ??
168
+ `http://${Sonamu.config.server.listen?.host ?? "localhost"}:${Sonamu.config.server.listen?.port ?? 3000}`,
169
+ };
170
+
164
171
  for (const target of targets) {
165
172
  // 지금 가져가려는 이 파일은 Sonamu 코드베이스의 일부입니다.
166
173
  // 그런데 dist 속 빌드된 소스 코드 파일이 필요한 것이 아니고, src에만 있는 텍스트 파일이 필요합니다.
@@ -178,6 +185,12 @@ export class Syncer {
178
185
  );
179
186
  }
180
187
 
188
+ const fullText = await readFile(srcPath, "utf-8");
189
+ const convertedText = Object.entries(convertMap).reduce(
190
+ (acc, [key, value]) => acc.replace(`$[[${key}]]`, value),
191
+ fullText,
192
+ );
193
+
181
194
  // 이건 프로젝트에 .ts 소스 코드 파일을 생성하는 것이므로 src의 .ts 경로로 갑니다.
182
195
  const destPath = path.join(Sonamu.appRootPath, target, "src/services/sonamu.shared.ts");
183
196
 
@@ -187,12 +200,11 @@ export class Syncer {
187
200
  console.warn(`Created directory '${path.dirname(destPath)}' because it did not exist.`);
188
201
  }
189
202
 
190
- if (await areFilesSame(srcPath, destPath)) {
203
+ if (await areFilesSame({ data: convertedText }, { path: destPath })) {
191
204
  continue;
192
205
  }
193
206
 
194
- await writeFile(destPath, await readFile(srcPath));
195
-
207
+ await writeFile(destPath, convertedText);
196
208
  !isTest() &&
197
209
  console.log(
198
210
  chalk.bold("Copied: ") + chalk.blue(path.relative(Sonamu.appRootPath, destPath)),
@@ -375,22 +387,7 @@ export class Syncer {
375
387
  }
376
388
 
377
389
  /**
378
- * sonamu.generated.ts와 sonamu.generated.sso.ts를 생성합니다.
379
- * @returns 생성된 파일 경로 배열.
380
- */
381
- async actionGenerateSchemas(): Promise<AbsolutePath[]> {
382
- return (
383
- await Promise.all([
384
- generateTemplate("generated_sso", {}, { overwrite: true }),
385
- generateTemplate("generated", {}, { overwrite: true }),
386
- ])
387
- )
388
- .flat()
389
- .flat();
390
- }
391
-
392
- /**
393
- * *.service.ts를 생성합니다.
390
+ * services.generated.ts를 생성합니다.
394
391
  * @param paramsArray
395
392
  * @returns 생성된 파일 경로 배열.
396
393
  */
@@ -400,14 +397,29 @@ export class Syncer {
400
397
  }[],
401
398
  ): Promise<string[]> {
402
399
  Naite.t("actionGenerateServices", paramsArray);
400
+
401
+ // services.generated.ts 통합 파일 생성
402
+ const servicesFile = await generateTemplate(
403
+ "services",
404
+ {},
405
+ {
406
+ overwrite: true,
407
+ },
408
+ );
409
+
410
+ return [...servicesFile];
411
+ }
412
+
413
+ /**
414
+ * sonamu.generated.ts와 sonamu.generated.sso.ts를 생성합니다.
415
+ * @returns 생성된 파일 경로 배열.
416
+ */
417
+ async actionGenerateSchemas(): Promise<AbsolutePath[]> {
403
418
  return (
404
- await Promise.all(
405
- paramsArray.map(async (params) =>
406
- generateTemplate("service", params as TemplateOptions["service"], {
407
- overwrite: true,
408
- }),
409
- ),
410
- )
419
+ await Promise.all([
420
+ generateTemplate("generated_sso", {}, { overwrite: true }),
421
+ generateTemplate("generated", {}, { overwrite: true }),
422
+ ])
411
423
  )
412
424
  .flat()
413
425
  .flat();
@@ -77,10 +77,14 @@ export class WorkflowManager {
77
77
  }
78
78
  >;
79
79
 
80
- private constructor(backend: BackendPostgres) {
80
+ // BackendPostgres에서 처리하는 것들이 있어서 Knex 커넥션이 아니라 설정값을 넣어줘야함.
81
+ constructor(dbConf: Knex.Config, runMigrations: boolean = true) {
82
+ const backend = new BackendPostgres(dbConf, { runMigrations });
83
+
81
84
  this.#backend = backend;
82
85
  this.#ow = new OpenWorkflow({ backend });
83
86
  this.#worker = null;
87
+
84
88
  this.#workflowsMap = new Map();
85
89
  this.#scheduledTasks = new Map();
86
90
  }
@@ -283,9 +287,18 @@ export class WorkflowManager {
283
287
  }
284
288
 
285
289
  // Worker를 설정 후 시작
286
- async setupWorker(options: WorkflowOptions) {
290
+ setupWorker(options: WorkflowOptions) {
287
291
  this.#worker = this.#ow.newWorker(options);
288
- await this.#worker.start();
292
+ }
293
+
294
+ // Worker를 초기화
295
+ async startWorker() {
296
+ if (!this.#worker) {
297
+ return;
298
+ }
299
+
300
+ await this.#backend.initialize();
301
+ await this.#worker?.start();
289
302
  }
290
303
 
291
304
  // Worker를 중지
@@ -318,13 +331,4 @@ export class WorkflowManager {
318
331
  [Symbol.asyncDispose]() {
319
332
  return this.destroy();
320
333
  }
321
-
322
- // BackendPostgres에서 처리하는 것들이 있어서 Knex 커넥션이 아니라 설정값을 넣어줘야함.
323
- static async create(
324
- dbConf: Knex.Config,
325
- runMigrations: boolean = true,
326
- ): Promise<WorkflowManager> {
327
- const backend = await BackendPostgres.connect(dbConf, { runMigrations });
328
- return new WorkflowManager(backend);
329
- }
330
334
  }
@@ -4,7 +4,12 @@ import { Sonamu } from "../../api";
4
4
  import type { Entity } from "../../entity/entity";
5
5
  import { EntityManager } from "../../entity/entity-manager";
6
6
  import { Naite } from "../../naite/naite";
7
- import { type EntityIndex, type EntityPropNode, isVirtualProp } from "../../types/types";
7
+ import {
8
+ type EntityIndex,
9
+ type EntityPropNode,
10
+ isVirtualCodeProp,
11
+ isVirtualQueryProp,
12
+ } from "../../types/types";
8
13
  import { nonNullable } from "../../utils/utils";
9
14
  import { Template } from "../template";
10
15
  import { propNodeToZodTypeDef, zodTypeToZodCode } from "../zod-converter";
@@ -171,9 +176,14 @@ export class Template__generated extends Template {
171
176
  // TODO: GIN/GiST 인덱스 생성된 컬럼 추출
172
177
  const fulltextColumns: EntityIndex["columns"][] = [];
173
178
 
174
- // virtual props
179
+ // virtual props (virtualType: "code" 또는 undefined인 것만 포함)
175
180
  const virtualProps = entity.props
176
- .filter((prop) => isVirtualProp(prop))
181
+ .filter((prop) => isVirtualCodeProp(prop))
182
+ .map((prop) => prop.name);
183
+
184
+ // query virtual props (virtualType: "query"인 것만 포함)
185
+ const virtualQueryProps = entity.props
186
+ .filter((prop) => isVirtualQueryProp(prop))
177
187
  .map((prop) => prop.name);
178
188
 
179
189
  /**
@@ -209,6 +219,7 @@ export class Template__generated extends Template {
209
219
  const hasMetadata =
210
220
  fulltextColumns.length > 0 ||
211
221
  virtualProps.length > 0 ||
222
+ virtualQueryProps.length > 0 ||
212
223
  hasDefaultColumns.length > 0 ||
213
224
  generatedColumns.length > 0 ||
214
225
  hasVectorColumns.length > 0;
@@ -226,6 +237,9 @@ export class Template__generated extends Template {
226
237
  (virtualProps.length > 0
227
238
  ? `readonly __virtual__: readonly [${virtualProps.map((prop) => `"${prop}"`).join(", ")}],`
228
239
  : "") +
240
+ (virtualQueryProps.length > 0
241
+ ? `readonly __virtual_query__: readonly [${virtualQueryProps.map((prop) => `"${prop}"`).join(", ")}],`
242
+ : "") +
229
243
  (hasDefaultColumns.length > 0
230
244
  ? `readonly __hasDefault__: readonly [${hasDefaultColumns
231
245
  .map((col) => `"${col}"`)
@@ -63,7 +63,7 @@ class ${entityId}ModelClass extends BaseModelClass<
63
63
  > {
64
64
  modelName = "${entityId}";
65
65
 
66
- @api({ httpMethod: "GET", clients: ["axios", "swr"], resourceName: "${entityId}" })
66
+ @api({ httpMethod: "GET", clients: ["axios", "tanstack-query"], resourceName: "${entityId}" })
67
67
  async findById<T extends ${entityId}SubsetKey>(
68
68
  subset: T,
69
69
  id: number
@@ -93,7 +93,7 @@ class ${entityId}ModelClass extends BaseModelClass<
93
93
  return rows[0] ?? null;
94
94
  }
95
95
 
96
- @api({ httpMethod: "GET", clients: ["axios", "swr"], resourceName: "${names.capitalPlural}" })
96
+ @api({ httpMethod: "GET", clients: ["axios", "tanstack-query"], resourceName: "${names.capitalPlural}" })
97
97
  async findMany<T extends ${entityId}SubsetKey, LP extends ${entityId}ListParams>(
98
98
  subset: T,
99
99
  rawParams?: LP,
@@ -156,7 +156,7 @@ class ${entityId}ModelClass extends BaseModelClass<
156
156
  });
157
157
  }
158
158
 
159
- @api({ httpMethod: "POST" })
159
+ @api({ httpMethod: "POST", clients: ["axios", "tanstack-mutation"] })
160
160
  async save(
161
161
  spa: ${entityId}SaveParams[]
162
162
  ): Promise<number[]> {
@@ -175,7 +175,7 @@ class ${entityId}ModelClass extends BaseModelClass<
175
175
  });
176
176
  }
177
177
 
178
- @api({ httpMethod: "POST", guards: [ "admin" ] })
178
+ @api({ httpMethod: "POST", clients: ["axios", "tanstack-mutation"], guards: [ "admin" ] })
179
179
  async del(ids: number[]): Promise<number> {
180
180
  const wdb = this.getPuri("w");
181
181