sonamu 0.7.16 → 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 (64) 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 +48 -38
  12. package/dist/syncer/checksum.d.ts +8 -3
  13. package/dist/syncer/checksum.d.ts.map +1 -1
  14. package/dist/syncer/checksum.js +17 -9
  15. package/dist/syncer/code-generator.js +7 -2
  16. package/dist/syncer/syncer.d.ts +6 -6
  17. package/dist/syncer/syncer.d.ts.map +1 -1
  18. package/dist/syncer/syncer.js +27 -13
  19. package/dist/template/implementations/model.template.js +5 -5
  20. package/dist/template/implementations/services.template.d.ts +17 -0
  21. package/dist/template/implementations/services.template.d.ts.map +1 -0
  22. package/dist/template/implementations/services.template.js +159 -0
  23. package/dist/template/implementations/view_form.template.js +2 -2
  24. package/dist/template/implementations/view_id_async_select.template.js +2 -2
  25. package/dist/template/implementations/view_list.template.js +5 -5
  26. package/dist/types/types.d.ts +2 -14
  27. package/dist/types/types.d.ts.map +1 -1
  28. package/dist/types/types.js +3 -15
  29. package/dist/ui/ai-api.d.ts +2 -0
  30. package/dist/ui/ai-api.d.ts.map +1 -1
  31. package/dist/ui/ai-api.js +43 -49
  32. package/dist/ui/ai-client.d.ts +10 -0
  33. package/dist/ui/ai-client.d.ts.map +1 -1
  34. package/dist/ui/ai-client.js +457 -437
  35. package/dist/ui/api.d.ts.map +1 -1
  36. package/dist/ui/api.js +3 -1
  37. package/dist/ui-web/assets/index-DzqUrTB-.js +92 -0
  38. package/dist/ui-web/index.html +1 -1
  39. package/package.json +11 -7
  40. package/src/api/config.ts +3 -0
  41. package/src/api/decorators.ts +6 -1
  42. package/src/api/sonamu.ts +68 -50
  43. package/src/shared/app.shared.ts.txt +1 -1
  44. package/src/shared/web.shared.ts.txt +0 -43
  45. package/src/syncer/checksum.ts +31 -9
  46. package/src/syncer/code-generator.ts +8 -1
  47. package/src/syncer/syncer.ts +38 -26
  48. package/src/template/implementations/model.template.ts +4 -4
  49. package/src/template/implementations/services.template.ts +226 -0
  50. package/src/template/implementations/view_form.template.ts +1 -1
  51. package/src/template/implementations/view_id_async_select.template.ts +1 -1
  52. package/src/template/implementations/view_list.template.ts +4 -4
  53. package/src/types/types.ts +2 -14
  54. package/src/ui/ai-api.ts +61 -60
  55. package/src/ui/ai-client.ts +535 -499
  56. package/src/ui/api.ts +3 -0
  57. package/src/ui/entity.instructions.md +536 -0
  58. package/dist/template/implementations/service.template.d.ts +0 -29
  59. package/dist/template/implementations/service.template.d.ts.map +0 -1
  60. package/dist/template/implementations/service.template.js +0 -202
  61. package/dist/ui-web/assets/index-BcbbB-BB.js +0 -95
  62. package/dist/ui-web/assets/provider-utils_false-BKJD46kk.js +0 -1
  63. package/dist/ui-web/assets/provider-utils_false-Bu5lmX18.js +0 -1
  64. package/src/template/implementations/service.template.ts +0 -328
@@ -0,0 +1,226 @@
1
+ import inflection from "inflection";
2
+ import { diff, unique } from "radashi";
3
+ import {
4
+ apiParamToTsCode,
5
+ apiParamTypeToTsType,
6
+ unwrapPromiseOnce,
7
+ } from "../../api/code-converters";
8
+ import type { ExtendedApi } from "../../api/decorators";
9
+ import { Sonamu } from "../../api/sonamu";
10
+ import type { TemplateOptions } from "../../types/types";
11
+ import { ApiParamType } from "../../types/types";
12
+ import { assertDefined } from "../../utils/utils";
13
+ import { Template } from "../template";
14
+
15
+ export class Template__services extends Template {
16
+ constructor() {
17
+ super("services");
18
+ }
19
+
20
+ getTargetAndPath() {
21
+ return {
22
+ target: ":target/src/services",
23
+ path: `services.generated.ts`,
24
+ };
25
+ }
26
+
27
+ render({}: TemplateOptions["services"]) {
28
+ const { apis } = Sonamu.syncer;
29
+
30
+ // 모델별로 그룹화
31
+ const apisByModel = new Map<string, ExtendedApi[]>();
32
+ for (const api of apis) {
33
+ const modelName = api.modelName.replace(/Model$/, "").replace(/Frame$/, "");
34
+ if (!apisByModel.has(modelName)) {
35
+ apisByModel.set(modelName, []);
36
+ }
37
+ apisByModel.get(modelName)?.push(api);
38
+ }
39
+
40
+ const importKeys: string[] = [];
41
+ const namespaces: string[] = [];
42
+ let typeParamNames: string[] = [];
43
+
44
+ for (const [modelName, modelApis] of apisByModel) {
45
+ const functions: string[] = [];
46
+
47
+ for (const api of modelApis) {
48
+ // Context 제외한 파라미터
49
+ const paramsWithoutContext = api.parameters.filter(
50
+ (param) =>
51
+ !ApiParamType.isContext(param.type) &&
52
+ !ApiParamType.isRefKnex(param.type) &&
53
+ !(param.optional === true && param.name.startsWith("_")),
54
+ );
55
+
56
+ // 타입 파라미터 정의
57
+ const typeParametersAsTsType = api.typeParameters
58
+ .map((typeParam) => apiParamTypeToTsType(typeParam, importKeys))
59
+ .join(", ");
60
+ const typeParamsDef = typeParametersAsTsType ? `<${typeParametersAsTsType}>` : "";
61
+ typeParamNames = typeParamNames.concat(api.typeParameters.map((tp) => tp.id));
62
+
63
+ // 파라미터 정의
64
+ const paramsDef = apiParamToTsCode(paramsWithoutContext, importKeys);
65
+ const paramNames = paramsWithoutContext.map((p) => p.name).join(", ");
66
+
67
+ // 리턴 타입 정의
68
+ const returnTypeDef = apiParamTypeToTsType(
69
+ assertDefined(unwrapPromiseOnce(api.returnType)),
70
+ importKeys,
71
+ );
72
+
73
+ // 기본 URL
74
+ const apiBaseUrl = `${Sonamu.config.api.route.prefix}${api.path}`;
75
+
76
+ const clients = api.options.clients || [];
77
+
78
+ // 1. axios 함수 생성
79
+ // resourceName이 있으면 get + resourceName 형태로 함수명 생성
80
+ const methodName = api.options.resourceName
81
+ ? `get${inflection.camelize(api.options.resourceName)}`
82
+ : api.methodName;
83
+
84
+ // axios-multipart 처리 (파일 업로드)
85
+ if (clients.includes("axios-multipart")) {
86
+ const isMultiple = api.uploadOptions?.mode === "multiple";
87
+ const fileParamName = isMultiple ? "files" : "file";
88
+ const fileParamType = isMultiple ? "File[]" : "File";
89
+
90
+ const formDataAppend = isMultiple
91
+ ? `${fileParamName}.forEach(f => { formData.append("${fileParamName}", f); });`
92
+ : `formData.append("${fileParamName}", ${fileParamName});`;
93
+
94
+ const otherParamsAppend = paramsWithoutContext
95
+ .map((param) => `formData.append('${param.name}', String(${param.name}));`)
96
+ .join("\n ");
97
+
98
+ const paramsDefComma = paramsDef !== "" ? ", " : "";
99
+ functions.push(
100
+ `
101
+ export async function ${methodName}${typeParamsDef}(
102
+ ${paramsDef}${paramsDefComma}
103
+ ${fileParamName}: ${fileParamType},
104
+ onUploadProgress?: (pe: AxiosProgressEvent) => void
105
+ ): Promise<${returnTypeDef}> {
106
+ const formData = new FormData();
107
+ ${formDataAppend}
108
+ ${otherParamsAppend}
109
+ return fetch({
110
+ method: 'POST',
111
+ url: \`${apiBaseUrl}\`,
112
+ headers: {
113
+ "Content-Type": "multipart/form-data",
114
+ },
115
+ onUploadProgress,
116
+ data: formData,
117
+ ${api.options.timeout ? `signal: AbortSignal.timeout(${api.options.timeout}),` : ""}
118
+ });
119
+ }
120
+ `.trim(),
121
+ );
122
+ } else if (api.options.httpMethod === "GET") {
123
+ const hasParams = paramsWithoutContext.length > 0;
124
+ functions.push(
125
+ `
126
+ export async function ${methodName}${typeParamsDef}(${paramsDef}): Promise<${returnTypeDef}> {
127
+ return fetch({
128
+ method: "GET",
129
+ url: \`${apiBaseUrl}${hasParams ? `?\${qs.stringify({ ${paramNames} })}` : ""}\`,
130
+ ${api.options.timeout ? `signal: AbortSignal.timeout(${api.options.timeout}),` : ""}
131
+ });
132
+ }
133
+ `.trim(),
134
+ );
135
+ } else {
136
+ const hasParams = paramsWithoutContext.length > 0;
137
+ functions.push(
138
+ `
139
+ export async function ${methodName}${typeParamsDef}(${paramsDef}): Promise<${returnTypeDef}> {
140
+ return fetch({
141
+ method: "${api.options.httpMethod}",
142
+ url: \`${apiBaseUrl}\`,
143
+ ${hasParams ? `data: { ${paramNames} },` : ""}
144
+ ${api.options.timeout ? `signal: AbortSignal.timeout(${api.options.timeout}),` : ""}
145
+ });
146
+ }
147
+ `.trim(),
148
+ );
149
+ }
150
+
151
+ // 2. queryOptions + useQuery (tanstack-query)
152
+ if (clients.includes("tanstack-query")) {
153
+ const hookName = api.options.resourceName
154
+ ? inflection.camelize(api.options.resourceName, true)
155
+ : inflection.camelize(api.methodName, true);
156
+
157
+ // queryOptions
158
+ functions.push(
159
+ `
160
+ export const ${methodName}QueryOptions = ${typeParamsDef}(${paramsDef}) => queryOptions({
161
+ queryKey: ['${modelName}', '${methodName}'${paramNames ? `, ${paramNames}` : ""}],
162
+ queryFn: () => ${methodName}(${paramNames})
163
+ });
164
+ `.trim(),
165
+ );
166
+
167
+ // useQuery hook
168
+ functions.push(
169
+ `
170
+ export const use${inflection.camelize(hookName)} = ${typeParamsDef}(${paramsDef}${
171
+ paramsDef ? ", " : ""
172
+ }options?: { enabled?: boolean }) =>
173
+ useQuery({
174
+ ...${methodName}QueryOptions(${paramNames}),
175
+ ...options
176
+ });
177
+ `.trim(),
178
+ );
179
+ }
180
+
181
+ // 3. useMutation (tanstack-mutation)
182
+ if (clients.includes("tanstack-mutation")) {
183
+ const hookName = inflection.camelize(api.methodName);
184
+ const mutationParamType =
185
+ paramsWithoutContext.length > 0
186
+ ? `{ ${paramsWithoutContext
187
+ .map((p) => `${p.name}: ${apiParamTypeToTsType(p.type, [])}`)
188
+ .join(", ")} }`
189
+ : "void";
190
+ const mutationParamNames =
191
+ paramsWithoutContext.length > 0
192
+ ? paramsWithoutContext.map((p) => `params.${p.name}`).join(", ")
193
+ : "";
194
+
195
+ functions.push(
196
+ `
197
+ export const use${hookName}Mutation = ${typeParamsDef}() => useMutation({
198
+ mutationFn: (params: ${mutationParamType}) => ${methodName}(${mutationParamNames})
199
+ });
200
+ `.trim(),
201
+ );
202
+ }
203
+ }
204
+
205
+ namespaces.push(
206
+ `
207
+ export namespace ${modelName}Service {
208
+ ${functions.join("\n\n")}
209
+ }
210
+ `.trim(),
211
+ );
212
+ }
213
+
214
+ return {
215
+ ...this.getTargetAndPath(),
216
+ body: namespaces.join("\n\n"),
217
+ importKeys: diff(unique(importKeys), [...typeParamNames, "ListResult"]),
218
+ customHeaders: [
219
+ `import { queryOptions, useQuery, useMutation } from '@tanstack/react-query';`,
220
+ `import type { AxiosProgressEvent } from 'axios';`,
221
+ `import qs from 'qs';`,
222
+ `import { type ListResult, fetch } from './sonamu.shared';`,
223
+ ],
224
+ };
225
+ }
226
+ }
@@ -256,7 +256,7 @@ import { defaultCatch } from '@/services/sonamu.shared';
256
256
  // import { useCommonModal } from "@/admin-common/CommonModal";
257
257
 
258
258
  import { ${names.capital}SaveParams } from '@/services/${names.fs}/${names.fs}.types';
259
- import { ${names.capital}Service } from '@/services/${names.fs}/${names.fs}.service';
259
+ import { ${names.capital}Service } from '@/services/services.generated';
260
260
  import { ${names.capital}SubsetA } from '@/services/sonamu.generated';
261
261
  ${unique(
262
262
  columns
@@ -40,7 +40,7 @@ import { DropdownProps, DropdownItemProps, DropdownOnSearchChangeData, Dropdown
40
40
  import { ${names.capital}SubsetKey, ${
41
41
  names.capital
42
42
  }SubsetMapping } from "@/services/sonamu.generated";
43
- import { ${names.capital}Service } from "@/services/${names.fs}/${names.fs}.service";
43
+ import { ${names.capital}Service } from "@/services/services.generated";
44
44
  import { ${names.capital}ListParams } from "@/services/${names.fs}/${names.fs}.types";
45
45
 
46
46
  export function ${names.capital}IdAsyncSelect<T extends ${names.capital}SubsetKey>(
@@ -326,7 +326,7 @@ import { DateTime } from "luxon";
326
326
  import { DelButton, EditButton, AppBreadcrumbs, AddButton, useSelection, useListParams, SonamuCol, numF, formatDate, formatDateTime } from '@sonamu-kit/react-sui';
327
327
 
328
328
  import { ${names.capital}SubsetA } from "@/services/sonamu.generated";
329
- import { ${names.capital}Service } from '@/services/${names.fs}/${names.fs}.service';
329
+ import { ${names.capital}Service } from '@/services/services.generated';
330
330
  import { ${names.capital}ListParams } from '@/services/${names.fs}/${names.fs}.types';
331
331
  ${columnImports}
332
332
  ${filterColumns
@@ -346,7 +346,7 @@ export default function ${names.capital}List({}: ${names.capital}ListProps) {
346
346
  });
347
347
 
348
348
  // 리스트 쿼리
349
- const { data, mutate, error, isLoading } = ${names.capital}Service.use${
349
+ const { data, refetch, isLoading } = ${names.capital}Service.use${
350
350
  names.capitalPlural
351
351
  }('A', listParams);
352
352
  const { rows, total } = data ?? {};
@@ -359,7 +359,7 @@ export default function ${names.capital}List({}: ${names.capital}ListProps) {
359
359
  }
360
360
 
361
361
  ${names.capital}Service.del(ids).then(() => {
362
- mutate();
362
+ refetch();
363
363
  });
364
364
  };
365
365
 
@@ -371,7 +371,7 @@ export default function ${names.capital}List({}: ${names.capital}ListProps) {
371
371
  }
372
372
 
373
373
  ${names.capital}Service.del(selectedKeys).then(() => {
374
- mutate();
374
+ refetch();
375
375
  });
376
376
  };
377
377
 
@@ -1237,19 +1237,7 @@ export const TemplateOptions = z.object({
1237
1237
  bridge: z.object({
1238
1238
  entityId: z.string(),
1239
1239
  }),
1240
- service: z.object({
1241
- namesRecord: z.object({
1242
- fs: z.string(),
1243
- fsPlural: z.string(),
1244
- camel: z.string(),
1245
- camelPlural: z.string(),
1246
- capital: z.string(),
1247
- capitalPlural: z.string(),
1248
- upper: z.string(),
1249
- constant: z.string(),
1250
- }),
1251
- modelTsPath: z.string(),
1252
- }),
1240
+ services: z.object({}),
1253
1241
  view_list: z.object({
1254
1242
  entityId: z.string(),
1255
1243
  extra: z.unknown(),
@@ -1302,7 +1290,7 @@ export const TemplateKey = z.enum([
1302
1290
  "model",
1303
1291
  "model_test",
1304
1292
  "bridge",
1305
- "service",
1293
+ "services",
1306
1294
  "view_list",
1307
1295
  "view_list_columns",
1308
1296
  "view_search_input",
package/src/ui/ai-api.ts CHANGED
@@ -1,60 +1,61 @@
1
- // import { convertToModelMessages, type UIMessage } from "ai";
2
- // import type { FastifyInstance } from "fastify";
3
- // import { BadRequestException, type FixtureRecord } from "sonamu";
4
- // import { aiClient } from "./ai-client";
5
-
6
- // export async function setAiApi(server: FastifyInstance) {
7
- // await aiClient.init();
8
-
9
- // server.post("/api/ai/fixture/chat", async (request, reply) => {
10
- // const { messages, fixtureRecords } = request.body as {
11
- // messages: UIMessage[];
12
- // fixtureRecords?: FixtureRecord[];
13
- // };
14
-
15
- // if (!fixtureRecords || fixtureRecords.length === 0) {
16
- // throw new BadRequestException("픽스쳐 레코드가 없습니다. 픽스쳐 조회 후 시도하세요.");
17
- // }
18
-
19
- // const result = aiClient.handleFixture(convertToModelMessages(messages), fixtureRecords);
20
- // const response = result.toUIMessageStreamResponse();
21
-
22
- // reply.raw.writeHead(response.status, Object.fromEntries(response.headers.entries()));
23
-
24
- // if (response.body) {
25
- // const reader = response.body.getReader();
26
- // while (true) {
27
- // const { done, value } = await reader.read();
28
- // if (done) break;
29
- // reply.raw.write(value);
30
- // }
31
- // }
32
-
33
- // reply.raw.end();
34
- // return reply;
35
- // });
36
-
37
- // // Entity/Enum 생성용 AI Chat Stream
38
- // server.post("/api/ai/entity/chat", async (request, reply) => {
39
- // const { messages } = request.body as {
40
- // messages: UIMessage[];
41
- // };
42
-
43
- // const result = aiClient.handleEntity(convertToModelMessages(messages));
44
- // const response = result.toUIMessageStreamResponse();
45
-
46
- // reply.raw.writeHead(response.status, Object.fromEntries(response.headers.entries()));
47
-
48
- // if (response.body) {
49
- // const reader = response.body.getReader();
50
- // while (true) {
51
- // const { done, value } = await reader.read();
52
- // if (done) break;
53
- // reply.raw.write(value);
54
- // }
55
- // }
56
-
57
- // reply.raw.end();
58
- // return reply;
59
- // });
60
- // }
1
+ import { convertToModelMessages, type UIMessage } from "ai";
2
+ import type { FastifyInstance } from "fastify";
3
+ import { BadRequestException } from "../exceptions/so-exceptions";
4
+ import type { FixtureRecord } from "../types/types";
5
+ import { aiClient } from "./ai-client";
6
+
7
+ export async function setAiApi(server: FastifyInstance) {
8
+ await aiClient.init();
9
+
10
+ server.post("/api/ai/fixture/chat", async (request, reply) => {
11
+ const { messages, fixtureRecords } = request.body as {
12
+ messages: UIMessage[];
13
+ fixtureRecords?: FixtureRecord[];
14
+ };
15
+
16
+ if (!fixtureRecords || fixtureRecords.length === 0) {
17
+ throw new BadRequestException("픽스쳐 레코드가 없습니다. 픽스쳐 조회 후 시도하세요.");
18
+ }
19
+
20
+ const result = aiClient.handleFixture(await convertToModelMessages(messages), fixtureRecords);
21
+ const response = result.toUIMessageStreamResponse();
22
+
23
+ reply.raw.writeHead(response.status, Object.fromEntries(Object.entries(response.headers)));
24
+
25
+ if (response.body) {
26
+ const reader = response.body.getReader();
27
+ while (true) {
28
+ const { done, value } = await reader.read();
29
+ if (done) break;
30
+ reply.raw.write(value);
31
+ }
32
+ }
33
+
34
+ reply.raw.end();
35
+ return reply;
36
+ });
37
+
38
+ // Entity/Enum 생성용 AI Chat Stream
39
+ server.post("/api/ai/entity/chat", async (request, reply) => {
40
+ const { messages } = request.body as {
41
+ messages: UIMessage[];
42
+ };
43
+
44
+ const result = aiClient.handleEntity(await convertToModelMessages(messages));
45
+ const response = result.toUIMessageStreamResponse();
46
+
47
+ reply.raw.writeHead(response.status, Object.fromEntries(Object.entries(response.headers)));
48
+
49
+ if (response.body) {
50
+ const reader = response.body.getReader();
51
+ while (true) {
52
+ const { done, value } = await reader.read();
53
+ if (done) break;
54
+ reply.raw.write(value);
55
+ }
56
+ }
57
+
58
+ reply.raw.end();
59
+ return reply;
60
+ });
61
+ }