sonamu 0.7.16 → 0.7.18

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 +180 -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-DFqVuxOB.js +92 -0
  38. package/dist/ui-web/index.html +1 -1
  39. package/package.json +9 -5
  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 +265 -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
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>{{projectName}}: Sonamu UI</title>
7
- <script type="module" crossorigin src="/sonamu-ui/assets/index-BcbbB-BB.js"></script>
7
+ <script type="module" crossorigin src="/sonamu-ui/assets/index-DFqVuxOB.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/sonamu-ui/assets/index-CpaB9P6g.css">
9
9
  </head>
10
10
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonamu",
3
- "version": "0.7.16",
3
+ "version": "0.7.18",
4
4
  "description": "Sonamu — TypeScript Fullstack API Framework",
5
5
  "keywords": [
6
6
  "typescript",
@@ -100,12 +100,13 @@
100
100
  "typescript": "^5.9.3"
101
101
  },
102
102
  "peerDependencies": {
103
- "@ai-sdk/openai": "3.0.0-beta.75",
104
- "@ai-sdk/provider": "3.0.0-beta.22",
105
- "@ai-sdk/provider-utils": "4.0.0-beta.40",
103
+ "@ai-sdk/openai": "3.0.0",
104
+ "@ai-sdk/provider": "3.0.0",
105
+ "@ai-sdk/provider-utils": "4.0.0",
106
+ "@ai-sdk/anthropic": "3.0.0",
106
107
  "@swc/cli": "^0.7.8",
107
108
  "@swc/core": "^1.13.5",
108
- "ai": "6.0.0-beta.138",
109
+ "ai": "6.0.1",
109
110
  "fastify": "^4.23.2",
110
111
  "knex": "^3.1.0",
111
112
  "pgvector": "^0.2.1",
@@ -122,6 +123,9 @@
122
123
  "@ai-sdk/provider-utils": {
123
124
  "optional": true
124
125
  },
126
+ "@ai-sdk/anthropic": {
127
+ "optional": true
128
+ },
125
129
  "ai": {
126
130
  "optional": true
127
131
  },
package/src/api/config.ts CHANGED
@@ -53,6 +53,9 @@ export type SonamuConfig = {
53
53
  };
54
54
 
55
55
  export type SonamuServerOptions = {
56
+ // 프로젝트 외부에서 접근할 수 있는 URL. 기본값은 {server.listen.host}:{server.listen.port} 입니다.
57
+ baseUrl?: string;
58
+
56
59
  fastify?: FastifyServerOptions;
57
60
 
58
61
  listen?: {
@@ -21,7 +21,12 @@ export interface GuardKeys {
21
21
  user: true;
22
22
  }
23
23
  export type GuardKey = keyof GuardKeys;
24
- export type ServiceClient = "axios" | "axios-multipart" | "swr" | "window-fetch";
24
+ export type ServiceClient =
25
+ | "axios"
26
+ | "axios-multipart"
27
+ | "tanstack-query"
28
+ | "tanstack-mutation"
29
+ | "window-fetch";
25
30
  export type ApiDecoratorOptions = {
26
31
  httpMethod?: HTTPMethods;
27
32
  contentType?:
package/src/api/sonamu.ts CHANGED
@@ -363,7 +363,7 @@ class SonamuClass {
363
363
  (api.options.httpMethod ?? "GET") === request.method.toUpperCase(),
364
364
  );
365
365
  if (found) {
366
- return this.getApiHandler(found, config)(request, reply);
366
+ return this.createApiHandler(found, config)(request, reply);
367
367
  }
368
368
 
369
369
  if (request.url.startsWith("/api/")) {
@@ -385,13 +385,13 @@ class SonamuClass {
385
385
  server.route({
386
386
  method: api.options.httpMethod ?? "GET",
387
387
  url: this.config.api.route.prefix + api.path,
388
- handler: this.getApiHandler(api, config),
388
+ handler: this.createApiHandler(api, config),
389
389
  }); // END server.route
390
390
  }
391
391
  }
392
392
  }
393
393
 
394
- getApiHandler(
394
+ createApiHandler(
395
395
  api: ExtendedApi,
396
396
  config: SonamuFastifyConfig,
397
397
  ): (request: FastifyRequest, reply: FastifyReply) => Promise<unknown> {
@@ -429,56 +429,74 @@ class SonamuClass {
429
429
  // Content-Type
430
430
  reply.type(api.options.contentType ?? "application/json");
431
431
 
432
- // createSSEFactory 함수에 미리 request의 socket과 reply를 바인딩.
433
- const { createSSEFactory } = await import("../stream/sse");
434
- const createSSE = (<T extends ZodObject>(
435
- _request: FastifyRequest,
436
- _reply: FastifyReply,
437
- _events: T,
438
- ) => createSSEFactory(_request.socket, _reply, _events)).bind(null, request, reply);
439
-
440
- const context: Context = {
441
- ...(await Promise.resolve(
442
- config.contextProvider(
443
- {
444
- request,
445
- reply,
446
- headers: request.headers,
447
- createSSE,
448
- naiteStore: Naite.createStore(),
449
- // auth
450
- user: request.user ?? null,
451
- passport: {
452
- login: request.login.bind(request) as AuthContext["passport"]["login"],
453
- logout: request.logout.bind(request) as AuthContext["passport"]["logout"],
454
- },
455
- },
456
- request,
457
- reply,
458
- ),
459
- )),
460
- };
461
-
462
- const model = this.syncer.models[api.modelName];
463
- return this.asyncLocalStorage.run({ context }, async () => {
464
- const { ApiParamType } = await import("../types/types");
465
- // biome-ignore lint/suspicious/noExplicitAny: model은 모델 인스턴스이므로 메서드 호출 가능
466
- const result = await (model as any)[api.methodName].apply(
467
- model,
468
- api.parameters.map((param) => {
469
- // Context 인젝션
470
- if (ApiParamType.isContext(param.type)) {
471
- return context;
472
- } else {
473
- return reqBody[param.name];
474
- }
475
- }),
476
- );
477
- reply.type(api.options.contentType ?? "application/json");
432
+ // Context 생성
433
+ const context: Context = await this.createContext(config, request, reply);
478
434
 
479
- return result;
435
+ // 모델 메소드 args 생성하여 호출
436
+ const { ApiParamType } = await import("../types/types");
437
+ const args = api.parameters.map((param) => {
438
+ // Context 인젝션
439
+ if (ApiParamType.isContext(param.type)) {
440
+ return context;
441
+ } else {
442
+ return reqBody[param.name];
443
+ }
480
444
  });
445
+ return this.invokeModelMethod(api, args, context, reply);
446
+ };
447
+ }
448
+
449
+ async invokeModelMethod(
450
+ api: ExtendedApi,
451
+ args: unknown[],
452
+ context: Context,
453
+ reply: FastifyReply,
454
+ ): Promise<unknown> {
455
+ const model = this.syncer.models[api.modelName];
456
+ return this.asyncLocalStorage.run({ context }, async () => {
457
+ // biome-ignore lint/suspicious/noExplicitAny: model은 모델 인스턴스이므로 메서드 호출 가능
458
+ const result = await (model as any)[api.methodName].apply(model, args);
459
+ reply.type(api.options.contentType ?? "application/json");
460
+
461
+ return result;
462
+ });
463
+ }
464
+
465
+ async createContext(
466
+ config: SonamuFastifyConfig,
467
+ request: FastifyRequest,
468
+ reply: FastifyReply,
469
+ ): Promise<Context> {
470
+ // createSSEFactory 함수에 미리 request의 socket과 reply를 바인딩.
471
+ const { createSSEFactory } = await import("../stream/sse");
472
+ const createSSE = (<T extends ZodObject>(
473
+ _request: FastifyRequest,
474
+ _reply: FastifyReply,
475
+ _events: T,
476
+ ) => createSSEFactory(_request.socket, _reply, _events)).bind(null, request, reply);
477
+
478
+ const context: Context = {
479
+ ...(await Promise.resolve(
480
+ config.contextProvider(
481
+ {
482
+ request,
483
+ reply,
484
+ headers: request.headers,
485
+ createSSE,
486
+ naiteStore: Naite.createStore(),
487
+ // auth
488
+ user: request.user ?? null,
489
+ passport: {
490
+ login: request.login.bind(request) as AuthContext["passport"]["login"],
491
+ logout: request.logout.bind(request) as AuthContext["passport"]["logout"],
492
+ },
493
+ },
494
+ request,
495
+ reply,
496
+ ),
497
+ )),
481
498
  };
499
+ return context;
482
500
  }
483
501
 
484
502
  async startWatcher(): Promise<void> {
@@ -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();
@@ -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