sonamu 0.7.53 → 0.8.0

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 (271) hide show
  1. package/dist/api/config.d.ts +9 -1
  2. package/dist/api/config.d.ts.map +1 -1
  3. package/dist/api/config.js +1 -1
  4. package/dist/api/sonamu.d.ts +21 -1
  5. package/dist/api/sonamu.d.ts.map +1 -1
  6. package/dist/api/sonamu.js +159 -65
  7. package/dist/auth/plugins/entity-definitions/anonymous.d.ts +10 -0
  8. package/dist/auth/plugins/entity-definitions/anonymous.d.ts.map +1 -0
  9. package/dist/auth/plugins/entity-definitions/anonymous.js +23 -0
  10. package/dist/auth/plugins/entity-definitions/api-key.d.ts +9 -0
  11. package/dist/auth/plugins/entity-definitions/api-key.d.ts.map +1 -0
  12. package/dist/auth/plugins/entity-definitions/api-key.js +199 -0
  13. package/dist/auth/plugins/entity-definitions/index.d.ts +6 -0
  14. package/dist/auth/plugins/entity-definitions/index.d.ts.map +1 -1
  15. package/dist/auth/plugins/entity-definitions/index.js +20 -2
  16. package/dist/auth/plugins/entity-definitions/jwt.d.ts +9 -0
  17. package/dist/auth/plugins/entity-definitions/jwt.d.ts.map +1 -0
  18. package/dist/auth/plugins/entity-definitions/jwt.js +67 -0
  19. package/dist/auth/plugins/entity-definitions/organization.d.ts +9 -0
  20. package/dist/auth/plugins/entity-definitions/organization.d.ts.map +1 -0
  21. package/dist/auth/plugins/entity-definitions/organization.js +424 -0
  22. package/dist/auth/plugins/entity-definitions/passkey.d.ts +10 -0
  23. package/dist/auth/plugins/entity-definitions/passkey.d.ts.map +1 -0
  24. package/dist/auth/plugins/entity-definitions/passkey.js +129 -0
  25. package/dist/auth/plugins/entity-definitions/sso.d.ts +10 -0
  26. package/dist/auth/plugins/entity-definitions/sso.d.ts.map +1 -0
  27. package/dist/auth/plugins/entity-definitions/sso.js +110 -0
  28. package/dist/auth/plugins/entity-definitions/types.d.ts +1 -1
  29. package/dist/auth/plugins/entity-definitions/types.d.ts.map +1 -1
  30. package/dist/auth/plugins/entity-definitions/types.js +1 -1
  31. package/dist/auth/plugins/wrappers/admin.d.ts.map +1 -1
  32. package/dist/auth/plugins/wrappers/admin.js +2 -4
  33. package/dist/auth/plugins/wrappers/anonymous.d.ts +18 -0
  34. package/dist/auth/plugins/wrappers/anonymous.d.ts.map +1 -0
  35. package/dist/auth/plugins/wrappers/anonymous.js +26 -0
  36. package/dist/auth/plugins/wrappers/api-key.d.ts +18 -0
  37. package/dist/auth/plugins/wrappers/api-key.d.ts.map +1 -0
  38. package/dist/auth/plugins/wrappers/api-key.js +38 -0
  39. package/dist/auth/plugins/wrappers/index.d.ts +6 -0
  40. package/dist/auth/plugins/wrappers/index.d.ts.map +1 -1
  41. package/dist/auth/plugins/wrappers/index.js +7 -1
  42. package/dist/auth/plugins/wrappers/jwt.d.ts +18 -0
  43. package/dist/auth/plugins/wrappers/jwt.d.ts.map +1 -0
  44. package/dist/auth/plugins/wrappers/jwt.js +30 -0
  45. package/dist/auth/plugins/wrappers/organization.d.ts +18 -0
  46. package/dist/auth/plugins/wrappers/organization.d.ts.map +1 -0
  47. package/dist/auth/plugins/wrappers/organization.js +67 -0
  48. package/dist/auth/plugins/wrappers/passkey.d.ts +18 -0
  49. package/dist/auth/plugins/wrappers/passkey.d.ts.map +1 -0
  50. package/dist/auth/plugins/wrappers/passkey.js +32 -0
  51. package/dist/auth/plugins/wrappers/phone-number.d.ts.map +1 -1
  52. package/dist/auth/plugins/wrappers/phone-number.js +2 -4
  53. package/dist/auth/plugins/wrappers/sso.d.ts +853 -0
  54. package/dist/auth/plugins/wrappers/sso.d.ts.map +1 -0
  55. package/dist/auth/plugins/wrappers/sso.js +36 -0
  56. package/dist/auth/plugins/wrappers/two-factor.d.ts.map +1 -1
  57. package/dist/auth/plugins/wrappers/two-factor.js +2 -4
  58. package/dist/auth/plugins/wrappers/username.d.ts.map +1 -1
  59. package/dist/auth/plugins/wrappers/username.js +2 -4
  60. package/dist/bin/build-config.d.ts +2 -2
  61. package/dist/bin/build-config.js +6 -7
  62. package/dist/bin/cli.js +417 -32
  63. package/dist/bin/fixture.d.ts +27 -0
  64. package/dist/bin/fixture.d.ts.map +1 -0
  65. package/dist/bin/fixture.js +245 -0
  66. package/dist/cache/decorator.d.ts +4 -3
  67. package/dist/cache/decorator.d.ts.map +1 -1
  68. package/dist/cache/decorator.js +5 -4
  69. package/dist/cone/cone-generator.d.ts +33 -0
  70. package/dist/cone/cone-generator.d.ts.map +1 -0
  71. package/dist/cone/cone-generator.js +286 -0
  72. package/dist/database/_batch_update.d.ts.map +1 -1
  73. package/dist/database/_batch_update.js +16 -2
  74. package/dist/database/puri-subset.test-d.js +1 -1
  75. package/dist/database/puri-subset.types.d.ts +1 -1
  76. package/dist/database/puri-subset.types.d.ts.map +1 -1
  77. package/dist/database/puri-subset.types.js +1 -1
  78. package/dist/database/puri.d.ts +4 -0
  79. package/dist/database/puri.d.ts.map +1 -1
  80. package/dist/database/puri.js +20 -2
  81. package/dist/database/upsert-builder.d.ts.map +1 -1
  82. package/dist/database/upsert-builder.js +19 -3
  83. package/dist/dict/en.d.ts +15 -0
  84. package/dist/dict/en.d.ts.map +1 -1
  85. package/dist/dict/en.js +2 -1
  86. package/dist/dict/ko.d.ts +15 -0
  87. package/dist/dict/ko.d.ts.map +1 -1
  88. package/dist/dict/ko.js +2 -1
  89. package/dist/dict/rc-keys.d.ts +28 -0
  90. package/dist/dict/rc-keys.d.ts.map +1 -1
  91. package/dist/dict/rc-keys.js +31 -1
  92. package/dist/dict/sd.d.ts.map +1 -1
  93. package/dist/dict/sd.js +20 -4
  94. package/dist/entity/entity-manager.d.ts +298 -2
  95. package/dist/entity/entity-manager.d.ts.map +1 -1
  96. package/dist/entity/entity-manager.js +4 -1
  97. package/dist/entity/entity-template-cone.d.ts +14 -0
  98. package/dist/entity/entity-template-cone.d.ts.map +1 -0
  99. package/dist/entity/entity-template-cone.js +222 -0
  100. package/dist/entity/entity.d.ts +47 -2
  101. package/dist/entity/entity.d.ts.map +1 -1
  102. package/dist/entity/entity.js +161 -14
  103. package/dist/ssr/renderer.js +3 -3
  104. package/dist/syncer/api-parser.js +12 -1
  105. package/dist/syncer/checksum.d.ts +0 -14
  106. package/dist/syncer/checksum.d.ts.map +1 -1
  107. package/dist/syncer/checksum.js +1 -23
  108. package/dist/syncer/syncer-actions.d.ts.map +1 -1
  109. package/dist/syncer/syncer-actions.js +8 -2
  110. package/dist/syncer/syncer.d.ts +1 -1
  111. package/dist/syncer/syncer.d.ts.map +1 -1
  112. package/dist/syncer/syncer.js +17 -10
  113. package/dist/tasks/workflow-manager.d.ts +13 -1
  114. package/dist/tasks/workflow-manager.d.ts.map +1 -1
  115. package/dist/tasks/workflow-manager.js +18 -1
  116. package/dist/template/entity-converter.js +4 -4
  117. package/dist/template/helpers.d.ts +10 -0
  118. package/dist/template/helpers.d.ts.map +1 -1
  119. package/dist/template/helpers.js +48 -1
  120. package/dist/template/implementations/entry-server.template.d.ts +1 -1
  121. package/dist/template/implementations/entry-server.template.js +7 -2
  122. package/dist/template/implementations/generated.template.d.ts.map +1 -1
  123. package/dist/template/implementations/generated.template.js +5 -1
  124. package/dist/template/implementations/generated_http.template.d.ts +1 -0
  125. package/dist/template/implementations/generated_http.template.d.ts.map +1 -1
  126. package/dist/template/implementations/generated_http.template.js +6 -2
  127. package/dist/template/implementations/generated_sso.template.d.ts.map +1 -1
  128. package/dist/template/implementations/generated_sso.template.js +29 -8
  129. package/dist/template/implementations/queries.template.d.ts.map +1 -1
  130. package/dist/template/implementations/queries.template.js +9 -1
  131. package/dist/template/implementations/sd.template.d.ts +1 -1
  132. package/dist/template/implementations/sd.template.d.ts.map +1 -1
  133. package/dist/template/implementations/sd.template.js +28 -4
  134. package/dist/template/implementations/services.template.d.ts.map +1 -1
  135. package/dist/template/implementations/services.template.js +12 -12
  136. package/dist/template/implementations/view_form.template.d.ts +11 -7
  137. package/dist/template/implementations/view_form.template.d.ts.map +1 -1
  138. package/dist/template/implementations/view_form.template.js +97 -87
  139. package/dist/template/implementations/view_list.template.d.ts +3 -3
  140. package/dist/template/implementations/view_list.template.d.ts.map +1 -1
  141. package/dist/template/implementations/view_list.template.js +115 -109
  142. package/dist/template/implementations/view_search_input.template.d.ts.map +1 -1
  143. package/dist/template/implementations/view_search_input.template.js +18 -14
  144. package/dist/template/zod-converter.d.ts.map +1 -1
  145. package/dist/template/zod-converter.js +95 -7
  146. package/dist/testing/_relation-graph.js +1 -1
  147. package/dist/testing/data-explorer.d.ts +61 -0
  148. package/dist/testing/data-explorer.d.ts.map +1 -0
  149. package/dist/testing/data-explorer.js +274 -0
  150. package/dist/testing/faker-mappings.d.ts +20 -0
  151. package/dist/testing/faker-mappings.d.ts.map +1 -0
  152. package/dist/testing/faker-mappings.js +421 -0
  153. package/dist/testing/fixture-generator.d.ts +161 -0
  154. package/dist/testing/fixture-generator.d.ts.map +1 -0
  155. package/dist/testing/fixture-generator.js +954 -0
  156. package/dist/testing/fixture-manager.d.ts +6 -1
  157. package/dist/testing/fixture-manager.d.ts.map +1 -1
  158. package/dist/testing/fixture-manager.js +72 -4
  159. package/dist/testing/index.d.ts +3 -0
  160. package/dist/testing/index.d.ts.map +1 -1
  161. package/dist/testing/index.js +4 -1
  162. package/dist/types/types.d.ts +1520 -26
  163. package/dist/types/types.d.ts.map +1 -1
  164. package/dist/types/types.js +136 -22
  165. package/dist/ui/ai-client.d.ts.map +1 -1
  166. package/dist/ui/ai-client.js +9 -4
  167. package/dist/ui/api.d.ts.map +1 -1
  168. package/dist/ui/api.js +303 -24
  169. package/dist/ui-web/assets/index-CsUr-_pV.js +254 -0
  170. package/dist/ui-web/assets/index-T42zzs1K.css +1 -0
  171. package/dist/ui-web/index.html +2 -2
  172. package/dist/utils/fs-utils.d.ts +2 -1
  173. package/dist/utils/fs-utils.d.ts.map +1 -1
  174. package/dist/utils/fs-utils.js +14 -3
  175. package/package.json +19 -11
  176. package/src/api/config.ts +12 -1
  177. package/src/api/sonamu.ts +179 -65
  178. package/src/auth/plugins/entity-definitions/anonymous.ts +17 -0
  179. package/src/auth/plugins/entity-definitions/api-key.ts +93 -0
  180. package/src/auth/plugins/entity-definitions/index.ts +18 -0
  181. package/src/auth/plugins/entity-definitions/jwt.ts +35 -0
  182. package/src/auth/plugins/entity-definitions/organization.ts +215 -0
  183. package/src/auth/plugins/entity-definitions/passkey.ts +64 -0
  184. package/src/auth/plugins/entity-definitions/sso.ts +62 -0
  185. package/src/auth/plugins/entity-definitions/types.ts +11 -1
  186. package/src/auth/plugins/wrappers/admin.ts +1 -3
  187. package/src/auth/plugins/wrappers/anonymous.ts +30 -0
  188. package/src/auth/plugins/wrappers/api-key.ts +42 -0
  189. package/src/auth/plugins/wrappers/index.ts +6 -0
  190. package/src/auth/plugins/wrappers/jwt.ts +34 -0
  191. package/src/auth/plugins/wrappers/organization.ts +73 -0
  192. package/src/auth/plugins/wrappers/passkey.ts +36 -0
  193. package/src/auth/plugins/wrappers/phone-number.ts +1 -3
  194. package/src/auth/plugins/wrappers/sso.ts +40 -0
  195. package/src/auth/plugins/wrappers/two-factor.ts +1 -3
  196. package/src/auth/plugins/wrappers/username.ts +1 -3
  197. package/src/bin/build-config.ts +6 -6
  198. package/src/bin/cli.ts +452 -31
  199. package/src/bin/fixture.ts +302 -0
  200. package/src/cache/decorator.ts +4 -3
  201. package/src/cone/cone-generator.ts +363 -0
  202. package/src/database/_batch_update.ts +11 -0
  203. package/src/database/puri-subset.test-d.ts +13 -13
  204. package/src/database/puri-subset.types.ts +1 -1
  205. package/src/database/puri.ts +43 -1
  206. package/src/database/upsert-builder.ts +16 -2
  207. package/src/dict/en.ts +1 -0
  208. package/src/dict/ko.ts +1 -0
  209. package/src/dict/rc-keys.ts +32 -0
  210. package/src/dict/sd.ts +23 -3
  211. package/src/entity/entity-manager.ts +4 -0
  212. package/src/entity/entity-template-cone.ts +298 -0
  213. package/src/entity/entity.ts +189 -13
  214. package/src/shared/app.shared.ts.txt +5 -0
  215. package/src/shared/web.shared.ts.txt +9 -5
  216. package/src/skills/project/README.md +21 -0
  217. package/src/skills/project/architecture.md +373 -0
  218. package/src/skills/project/business-logic.md +270 -0
  219. package/src/skills/project/requirements.md +160 -0
  220. package/src/skills/sonamu/SKILL.md +168 -3
  221. package/src/skills/sonamu/api.md +102 -0
  222. package/src/skills/sonamu/database.md +220 -1
  223. package/src/skills/sonamu/entity-relations.md +89 -1
  224. package/src/skills/sonamu/fixture-cli.md +501 -0
  225. package/src/skills/sonamu/frontend.md +214 -0
  226. package/src/skills/sonamu/i18n.md +95 -0
  227. package/src/skills/sonamu/model.md +153 -0
  228. package/src/skills/sonamu/project-init.md +178 -8
  229. package/src/skills/sonamu/scaffolding.md +112 -0
  230. package/src/skills/sonamu/subset.md +9 -3
  231. package/src/skills/sonamu/testing.md +287 -2
  232. package/src/skills/sonamu/workflow.md +70 -5
  233. package/src/ssr/renderer.ts +2 -2
  234. package/src/syncer/api-parser.ts +12 -0
  235. package/src/syncer/checksum.ts +0 -38
  236. package/src/syncer/syncer-actions.ts +7 -1
  237. package/src/syncer/syncer.ts +16 -5
  238. package/src/tasks/workflow-manager.ts +29 -8
  239. package/src/template/entity-converter.ts +3 -3
  240. package/src/template/helpers.ts +49 -0
  241. package/src/template/implementations/entry-server.template.ts +1 -1
  242. package/src/template/implementations/generated.template.ts +4 -0
  243. package/src/template/implementations/generated_http.template.ts +1 -0
  244. package/src/template/implementations/generated_sso.template.ts +40 -11
  245. package/src/template/implementations/queries.template.ts +8 -0
  246. package/src/template/implementations/sd.template.ts +22 -3
  247. package/src/template/implementations/services.template.ts +11 -10
  248. package/src/template/implementations/view_form.template.ts +111 -101
  249. package/src/template/implementations/view_list.template.ts +120 -119
  250. package/src/template/implementations/view_search_input.template.ts +17 -13
  251. package/src/template/zod-converter.ts +103 -6
  252. package/src/testing/_relation-graph.ts +1 -1
  253. package/src/testing/data-explorer.ts +427 -0
  254. package/src/testing/faker-mappings.ts +434 -0
  255. package/src/testing/fixture-generator.ts +1166 -0
  256. package/src/testing/fixture-manager.ts +91 -6
  257. package/src/testing/index.ts +3 -0
  258. package/src/types/types.ts +222 -26
  259. package/src/ui/ai-client.ts +9 -1
  260. package/src/ui/api.ts +429 -23
  261. package/src/utils/fs-utils.ts +14 -1
  262. package/dist/template/implementations/view_enums_select.template.d.ts +0 -17
  263. package/dist/template/implementations/view_enums_select.template.d.ts.map +0 -1
  264. package/dist/template/implementations/view_enums_select.template.js +0 -62
  265. package/dist/template/implementations/view_id_async_select.template.d.ts +0 -17
  266. package/dist/template/implementations/view_id_async_select.template.d.ts.map +0 -1
  267. package/dist/template/implementations/view_id_async_select.template.js +0 -125
  268. package/dist/ui-web/assets/index-Bd_2AkLb.css +0 -1
  269. package/dist/ui-web/assets/index-BpSbhQWo.js +0 -225
  270. package/src/template/implementations/view_enums_select.template.ts +0 -65
  271. package/src/template/implementations/view_id_async_select.template.ts +0 -139
package/src/api/sonamu.ts CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  DB,
19
19
  isDaemonServer,
20
20
  merge,
21
+ NotFoundException,
21
22
  } from "..";
22
23
  import type { CacheConfig, CacheManager } from "../cache/types";
23
24
  import { applyCacheHeaders, CachePresets } from "../cache-control/cache-control";
@@ -25,6 +26,7 @@ import type { CacheControlConfig, CacheControlRequest } from "../cache-control/t
25
26
  import { toFastifyCompressOption } from "../compress/compress";
26
27
  import type { CompressOptions } from "../compress/types";
27
28
  import type { SonamuDBConfig } from "../database/db";
29
+ import { SD } from "../dict/sd";
28
30
  import type { LocalizedString } from "../dict/types";
29
31
  import { Naite } from "../naite/naite";
30
32
  import { BufferedFile } from "../storage/buffered-file";
@@ -379,7 +381,6 @@ class SonamuClass {
379
381
  server.register(sonamuUIApiPlugin);
380
382
  }
381
383
 
382
- // 로컬/프로덕션 환경 분기
383
384
  const webPath = path.join(this.appRootPath, "web");
384
385
  const hasWeb = await exists(webPath);
385
386
 
@@ -396,9 +397,13 @@ class SonamuClass {
396
397
  : undefined;
397
398
 
398
399
  if (isLocal()) {
399
- // 로컬 개발 환경: Vite Dev Server + 통합 핸들러
400
- if (hasWeb) {
401
- await this.setupViteDevServer(server, webPath, config, globalCompressOptions);
400
+ // 로컬 개발 환경: catch-all로 API를 동적 매칭하여 HMR을 지원합니다.
401
+ // SONAMU_DISABLE_INTEGRATED_WEB=yes로 설정하면 dev_api 모드에서 Vite 통합을 비활성화할 수 있습니다.
402
+ const disableIntegratedWeb = process.env.SONAMU_DISABLE_INTEGRATED_WEB === "yes";
403
+ if (hasWeb && !disableIntegratedWeb) {
404
+ await this.setupDevServerWithVite(server, webPath, config);
405
+ } else {
406
+ this.setupDevServer(server, config);
402
407
  }
403
408
  } else {
404
409
  // 프로덕션 환경: 개별 API 라우트 + 정적 파일 서빙
@@ -415,20 +420,84 @@ class SonamuClass {
415
420
  });
416
421
  }
417
422
 
418
- if (hasWeb) {
419
- await this.setupStaticWebServer(server, webPath, config, globalCompressOptions);
423
+ // 프로덕션에서는 web 소스(appRoot/web) 유무와 무관하게,
424
+ // api/web-dist 존재 여부를 setupStaticWebServer 내부에서 판단합니다.
425
+ await this.setupStaticWebServer(server, config, globalCompressOptions);
426
+ }
427
+ }
428
+
429
+ /**
430
+ * dev 모드 공통: catch-all에서 syncer.apis를 동적으로 탐색하여 API 요청을 처리합니다.
431
+ * server.route()로 개별 등록하면 handler가 고정되어 HMR이 동작하지 않으므로,
432
+ * 매 요청마다 syncer.apis를 조회하는 이 방식을 사용합니다.
433
+ *
434
+ * 요청이 /api(정확히는 this.config.api.route.prefix)로 시작하지 않는 경우라면 null을 반환하며 끝냅니다.
435
+ */
436
+ private handleDevApiRequest(
437
+ request: FastifyRequest,
438
+ config: SonamuFastifyConfig,
439
+ ): ((request: FastifyRequest, reply: FastifyReply) => Promise<unknown>) | null {
440
+ const url = this.getPathnameFromUrl(request.url);
441
+ const method = request.method;
442
+
443
+ if (!url.startsWith(this.config.api.route.prefix)) {
444
+ return null;
445
+ }
446
+
447
+ // syncer.apis의 path는 :param 형태를 포함할 수 있으므로 세그먼트 단위로 매칭합니다.
448
+ // 정규식 생성 방식은 path 문자열 내 특수문자(., +, (, [ 등)로 오작동할 수 있어 사용하지 않습니다.
449
+ const matchedApi = this.syncer.apis.find((api) => {
450
+ if (this.syncer.models[api.modelName] === undefined) {
451
+ return false;
420
452
  }
453
+ const apiMethod = api.options.httpMethod ?? "GET";
454
+ if (apiMethod !== method) return false;
455
+
456
+ const fullPath = this.config.api.route.prefix + api.path;
457
+ return this.isPathPatternMatch(fullPath, url);
458
+ });
459
+
460
+ if (!matchedApi) {
461
+ throw new NotFoundException(SD("error.api.notFound"));
421
462
  }
463
+
464
+ return this.createApiHandler(matchedApi, config);
465
+ }
466
+
467
+ /**
468
+ * dev api 모드: Vite 없이 API 동적 라우팅만 제공합니다.
469
+ * HMR을 위해 catch-all에서 매 요청마다 syncer.apis를 조회합니다.
470
+ */
471
+ private setupDevServer(
472
+ server: FastifyInstance<Server, IncomingMessage, ServerResponse>,
473
+ config: SonamuFastifyConfig,
474
+ ): void {
475
+ server.route({
476
+ method: ["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"],
477
+ url: `${this.config.api.route.prefix}/*`,
478
+ handler: async (request, reply) => {
479
+ const handler = this.handleDevApiRequest(request, config);
480
+ if (handler) {
481
+ return handler(request, reply);
482
+ }
483
+ // 사실 /api로 시작하지 않는 요청은 여기에 들어오지도 않을 거라 이 라인은 도달 불가능입니다만,
484
+ // 안전빵으로 남겨놓습니다.
485
+ throw new NotFoundException(SD("error.api.notFound"));
486
+ },
487
+ });
422
488
  }
423
489
 
424
490
  // biome-ignore lint/suspicious/noExplicitAny: ViteDevServer 타입을 동적으로 로드해야 함
425
491
  private viteServer: any = null;
426
492
 
427
- private async setupViteDevServer(
493
+ /**
494
+ * dev all 모드: Vite Dev Server를 통합하여 API + SSR + CSR을 모두 제공합니다.
495
+ * API 동적 매칭은 handleDevApiRequest를 공유합니다.
496
+ */
497
+ private async setupDevServerWithVite(
428
498
  server: FastifyInstance<Server, IncomingMessage, ServerResponse>,
429
499
  webPath: string,
430
500
  config: SonamuFastifyConfig,
431
- globalCompressOptions?: CompressOptions,
432
501
  ): Promise<void> {
433
502
  // @fastify/middie 등록 (Connect-style middleware 지원)
434
503
  await server.register((await import("@fastify/middie")).default);
@@ -456,49 +525,39 @@ class SonamuClass {
456
525
  return this.viteServer.middlewares(req, res, next);
457
526
  });
458
527
 
459
- // API 동적 라우팅 (catch-all 전에 등록)
460
- for (const api of this.syncer.apis) {
461
- if (this.syncer.models[api.modelName] === undefined) {
462
- throw new Error(`정의되지 않은 모델에 접근 ${api.modelName}`);
463
- }
464
-
465
- server.route({
466
- method: api.options.httpMethod ?? "GET",
467
- url: this.config.api.route.prefix + api.path,
468
- handler: this.createApiHandler(api, config),
469
- compress: toFastifyCompressOption(api.options.compress, globalCompressOptions),
470
- });
471
- }
472
-
473
- // SSR 라우트 개별 등록 (compress 옵션이 라우트별로 적용되도록)
474
- const { getSSRRoutes, renderSSR } = await import("../ssr");
475
- const ssrRoutes = getSSRRoutes();
476
-
477
- for (const route of ssrRoutes) {
478
- server.route({
479
- method: ["GET", "HEAD"],
480
- url: route.path,
481
- compress: toFastifyCompressOption(route.compress ?? true, globalCompressOptions),
482
- handler: async (request, reply) => {
483
- const url = request.url;
484
- console.log(`[SSR] Matched route: ${route.path}`);
485
-
486
- const params = this.extractPathParams(route.path, url);
487
- const html = await renderSSR(url, route, params, request, reply, config, this.viteServer);
488
-
489
- reply.type("text/html");
490
- return html;
491
- },
492
- });
493
- }
494
-
495
- // CSR fallback (SSR 라우트에 매칭되지 않는 모든 요청)
528
+ // catch-all 라우트에서 동적으로 API/SSR 처리
529
+ // 개발 환경에서는 라우트별 compress 옵션을 포기하고 HMR 이점을 취합니다.
496
530
  server.route({
497
- method: ["GET", "HEAD"],
531
+ method: ["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"],
498
532
  url: "/*",
499
533
  handler: async (request, reply) => {
534
+ // 1. API 요청 처리
535
+ const result = this.handleDevApiRequest(request, config);
536
+ if (result) {
537
+ return result(request, reply);
538
+ }
539
+
500
540
  const url = request.url;
501
541
 
542
+ // 2. SSR 라우트 처리
543
+ const { matchSSRRoute, renderSSR } = await import("../ssr");
544
+ const ssrMatch = matchSSRRoute(url);
545
+ if (ssrMatch) {
546
+ console.log(`[SSR] Matched route: ${ssrMatch.route.path}`);
547
+ const html = await renderSSR(
548
+ url,
549
+ ssrMatch.route,
550
+ ssrMatch.params,
551
+ request,
552
+ reply,
553
+ config,
554
+ this.viteServer,
555
+ );
556
+ reply.type("text/html");
557
+ return html;
558
+ }
559
+
560
+ // 3. CSR fallback
502
561
  try {
503
562
  const fs = await import("node:fs/promises");
504
563
  let template = await fs.readFile(
@@ -528,15 +587,14 @@ class SonamuClass {
528
587
 
529
588
  private async setupStaticWebServer(
530
589
  server: FastifyInstance<Server, IncomingMessage, ServerResponse>,
531
- _webPath: string,
532
590
  config: SonamuFastifyConfig,
533
591
  globalCompressOptions: CompressOptions | undefined,
534
592
  ): Promise<void> {
535
- // 경로 명확화: api/public/web, api/dist/ssr
536
- const webDistPath = path.join(this.apiRootPath, "public", "web");
537
- const ssrPath = path.join(this.apiRootPath, "dist", "ssr");
593
+ // 경로 명확화: api/web-dist/client (정적 파일), api/web-dist/server (SSR entry), api/dist/ssr (SSR routes - API 소유)
594
+ const webDistPath = path.join(this.apiRootPath, "web-dist", "client");
595
+ const ssrPath = path.join(this.apiRootPath, "web-dist", "server");
538
596
  const ssrEntryPath = path.join(ssrPath, "entry-server.generated.js");
539
- const ssrRoutesPath = path.join(ssrPath, "routes.js");
597
+ const ssrRoutesPath = path.join(this.apiRootPath, "dist", "ssr", "routes.js");
540
598
 
541
599
  if (!(await exists(webDistPath))) {
542
600
  console.warn(`⚠ Web dist not found: ${webDistPath}`);
@@ -568,7 +626,14 @@ class SonamuClass {
568
626
  server.get("/assets/:filename", async (request, reply) => {
569
627
  const requestedFile = (request.params as { filename: string }).filename;
570
628
  const assetsDir = path.join(webDistPath, "assets");
571
- const assetPath = `/assets/${requestedFile}`;
629
+ const safeFilePath = this.resolvePathWithinBaseDir(assetsDir, requestedFile);
630
+ if (safeFilePath === null) {
631
+ reply.status(403).send();
632
+ return;
633
+ }
634
+ const normalizedRequestedFile = path.relative(assetsDir, safeFilePath).replace(/\\/g, "/");
635
+
636
+ const assetPath = `/assets/${normalizedRequestedFile}`;
572
637
 
573
638
  // Cache-Control 헤더 결정
574
639
  const getCacheControlForAsset = (): CacheControlConfig => {
@@ -590,8 +655,8 @@ class SonamuClass {
590
655
  };
591
656
 
592
657
  // index-*.js 또는 index-*.css 요청인 경우
593
- if (/^index-[a-f0-9]+\.(js|css)$/.test(requestedFile)) {
594
- const ext = requestedFile.split(".").pop();
658
+ if (/^index-[a-f0-9]+\.(js|css)$/.test(normalizedRequestedFile)) {
659
+ const ext = normalizedRequestedFile.split(".").pop();
595
660
  const files = await fs.readdir(assetsDir);
596
661
  const currentFile = files.find((f) => f.startsWith("index-") && f.endsWith(`.${ext}`));
597
662
 
@@ -605,18 +670,18 @@ class SonamuClass {
605
670
  }
606
671
 
607
672
  // 일반 파일 서빙
608
- const filePath = path.join(assetsDir, requestedFile);
673
+ const filePath = safeFilePath;
609
674
  if (await exists(filePath)) {
610
675
  const content = await fs.readFile(filePath);
611
- const ext = requestedFile.split(".").pop();
676
+ const ext = normalizedRequestedFile.split(".").pop();
612
677
  reply.type(ext === "js" ? "application/javascript" : ext === "css" ? "text/css" : "");
613
- if (requestedFile.includes("-")) {
678
+ if (normalizedRequestedFile.includes("-")) {
614
679
  applyCacheHeaders(reply, getCacheControlForAsset());
615
680
  }
616
681
  return reply.send(content);
617
682
  }
618
683
 
619
- reply.code(404).send("Not found");
684
+ reply.status(404).send();
620
685
  });
621
686
 
622
687
  // SSR 라우트 개별 등록 (compress 옵션이 라우트별로 적용되도록)
@@ -651,7 +716,7 @@ class SonamuClass {
651
716
  handler: async (request, reply) => {
652
717
  // /api, /sonamu-ui는 404 그대로
653
718
  if (request.url.startsWith("/api") || request.url.startsWith("/sonamu-ui")) {
654
- reply.code(404).send({ error: "Not Found" });
719
+ reply.status(404).send();
655
720
  return;
656
721
  }
657
722
 
@@ -671,10 +736,15 @@ class SonamuClass {
671
736
  }
672
737
 
673
738
  // 정적 파일이 존재할 경우, 정적 파일을 먼저 서빙해야함
674
- const filePath = path.join(webDistPath, request.url);
675
- if (await fileExists(filePath)) {
676
- const content = await fs.readFile(filePath);
677
- return reply.type(mimeLookup(filePath) || "application/octet-stream").send(content);
739
+ const requestPath = this.getPathnameFromUrl(request.url);
740
+ const safeFilePath = this.resolvePathWithinBaseDir(webDistPath, requestPath);
741
+ if (safeFilePath === null) {
742
+ reply.status(403).send();
743
+ return;
744
+ }
745
+ if (await fileExists(safeFilePath)) {
746
+ const content = await fs.readFile(safeFilePath);
747
+ return reply.type(mimeLookup(safeFilePath) || "application/octet-stream").send(content);
678
748
  }
679
749
 
680
750
  // CSR fallback: index.html 서빙
@@ -845,7 +915,7 @@ class SonamuClass {
845
915
  */
846
916
  private extractPathParams(pattern: string, url: string): Record<string, string> {
847
917
  const patternParts = pattern.split("/").filter(Boolean);
848
- const urlParts = url.split("?")[0].split("/").filter(Boolean);
918
+ const urlParts = this.getPathnameFromUrl(url).split("/").filter(Boolean);
849
919
  const params: Record<string, string> = {};
850
920
 
851
921
  for (let i = 0; i < patternParts.length; i++) {
@@ -856,6 +926,50 @@ class SonamuClass {
856
926
  return params;
857
927
  }
858
928
 
929
+ private isPathPatternMatch(pattern: string, url: string): boolean {
930
+ const patternParts = pattern.split("/").filter(Boolean);
931
+ const urlParts = this.getPathnameFromUrl(url).split("/").filter(Boolean);
932
+
933
+ if (patternParts.length !== urlParts.length) {
934
+ return false;
935
+ }
936
+
937
+ for (let i = 0; i < patternParts.length; i++) {
938
+ const patternPart = patternParts[i];
939
+ const urlPart = urlParts[i];
940
+ if (patternPart.startsWith(":")) {
941
+ continue;
942
+ }
943
+ if (patternPart !== urlPart) {
944
+ return false;
945
+ }
946
+ }
947
+
948
+ return true;
949
+ }
950
+
951
+ private getPathnameFromUrl(url: string): string {
952
+ return url.split("?")[0];
953
+ }
954
+
955
+ private resolvePathWithinBaseDir(baseDir: string, inputPath: string): string | null {
956
+ try {
957
+ const decoded = decodeURIComponent(inputPath).replace(/\\/g, "/");
958
+ if (decoded.includes("\0")) {
959
+ return null;
960
+ }
961
+ const relativePath = decoded.replace(/^\/+/, "");
962
+ const resolvedPath = path.resolve(baseDir, relativePath);
963
+ const relativeFromBase = path.relative(baseDir, resolvedPath);
964
+ if (relativeFromBase.startsWith("..") || path.isAbsolute(relativeFromBase)) {
965
+ return null;
966
+ }
967
+ return resolvedPath;
968
+ } catch {
969
+ return null;
970
+ }
971
+ }
972
+
859
973
  /**
860
974
  * API 응답에 적용할 Cache-Control 설정을 결정합니다.
861
975
  * 우선순위: 개별 지정 > cacheControlHandler
@@ -1037,7 +1151,7 @@ class SonamuClass {
1037
1151
  }
1038
1152
 
1039
1153
  /*
1040
- A function that automatically handles init and destroy when using Sonamu via scripts.
1154
+ A function that automatically handles init and destroy when using Sonamu via scripts.
1041
1155
  */
1042
1156
  async runScript(fn: () => Promise<void>) {
1043
1157
  await this.init(true, false, undefined, false);
@@ -0,0 +1,17 @@
1
+ import type { BetterAuthEntityDef } from "./types";
2
+
3
+ /**
4
+ * better-auth Anonymous 플러그인 엔티티 정의
5
+ * https://www.better-auth.com/docs/plugins/anonymous
6
+ *
7
+ * 익명 사용자 인증을 지원합니다.
8
+ * 새로운 테이블을 생성하지 않고 User 테이블에 is_anonymous 필드만 추가합니다.
9
+ */
10
+ export const anonymousEntityDef: BetterAuthEntityDef = {
11
+ id: "anonymous",
12
+ name: "Anonymous",
13
+ entities: [],
14
+ additionalProps: {
15
+ User: [{ name: "is_anonymous", type: "boolean", nullable: true, desc: "익명 사용자 여부" }],
16
+ },
17
+ };
@@ -0,0 +1,93 @@
1
+ import type { BetterAuthEntityDef } from "./types";
2
+
3
+ /**
4
+ * better-auth API Key 플러그인 엔티티 정의
5
+ * https://www.better-auth.com/docs/plugins/api-key
6
+ *
7
+ * API 키 인증을 지원합니다.
8
+ */
9
+ export const apiKeyEntityDef: BetterAuthEntityDef = {
10
+ id: "api-key",
11
+ name: "API Key",
12
+ entities: [
13
+ {
14
+ id: "ApiKey",
15
+ table: "api_keys",
16
+ title: "API 키",
17
+ props: [
18
+ { name: "id", type: "string", desc: "ID" },
19
+ { name: "key", type: "string", desc: "해시된 API 키" },
20
+ { name: "start", type: "string", nullable: true, desc: "키 시작 문자열" },
21
+ { name: "prefix", type: "string", nullable: true, desc: "키 접두사" },
22
+ { name: "name", type: "string", nullable: true, desc: "키 이름" },
23
+ { name: "remaining", type: "integer", nullable: true, desc: "남은 요청 수" },
24
+ { name: "last_request", type: "date", nullable: true, desc: "마지막 요청 시간" },
25
+ { name: "request_count", type: "integer", desc: "요청 횟수" },
26
+ { name: "rate_limit_enabled", type: "boolean", desc: "Rate Limit 활성화 여부" },
27
+ {
28
+ name: "rate_limit_time_window",
29
+ type: "integer",
30
+ nullable: true,
31
+ desc: "Rate Limit 시간 창 (ms)",
32
+ },
33
+ {
34
+ name: "rate_limit_max",
35
+ type: "integer",
36
+ nullable: true,
37
+ desc: "Rate Limit 최대 요청 수",
38
+ },
39
+ { name: "refill_interval", type: "integer", nullable: true, desc: "리필 간격 (ms)" },
40
+ { name: "refill_amount", type: "integer", nullable: true, desc: "리필 양" },
41
+ { name: "last_refill_at", type: "date", nullable: true, desc: "마지막 리필 시간" },
42
+ { name: "expires_at", type: "date", nullable: true, desc: "만료일시" },
43
+ { name: "enabled", type: "boolean", desc: "활성화 여부" },
44
+ { name: "permissions", type: "string", nullable: true, desc: "권한" },
45
+ { name: "metadata", type: "string", nullable: true, desc: "메타데이터 (JSON)" },
46
+ { name: "created_at", type: "date", dbDefault: "CURRENT_TIMESTAMP", desc: "생성일시" },
47
+ { name: "updated_at", type: "date", nullable: true, desc: "수정일시" },
48
+ {
49
+ type: "relation",
50
+ name: "user",
51
+ with: "User",
52
+ relationType: "BelongsToOne",
53
+ onDelete: "CASCADE",
54
+ desc: "사용자",
55
+ },
56
+ ],
57
+ indexes: [
58
+ { type: "index", name: "api_keys_user_id_idx", columns: [{ name: "user_id" }] },
59
+ { type: "unique", name: "api_keys_key_unique", columns: [{ name: "key" }] },
60
+ ],
61
+ subsets: {
62
+ A: [
63
+ "id",
64
+ "key",
65
+ "start",
66
+ "prefix",
67
+ "name",
68
+ "remaining",
69
+ "last_request",
70
+ "request_count",
71
+ "rate_limit_enabled",
72
+ "rate_limit_time_window",
73
+ "rate_limit_max",
74
+ "refill_interval",
75
+ "refill_amount",
76
+ "last_refill_at",
77
+ "expires_at",
78
+ "enabled",
79
+ "permissions",
80
+ "metadata",
81
+ "created_at",
82
+ "updated_at",
83
+ "user.id",
84
+ ],
85
+ },
86
+ enums: {
87
+ ApiKeyOrderBy: { "id-desc": "ID최신순", "created_at-desc": "생성일최신순" },
88
+ ApiKeySearchField: { id: "ID", name: "이름" },
89
+ },
90
+ },
91
+ ],
92
+ additionalProps: {},
93
+ };
@@ -1,11 +1,23 @@
1
1
  export { adminEntityDef } from "./admin";
2
+ export { anonymousEntityDef } from "./anonymous";
3
+ export { apiKeyEntityDef } from "./api-key";
4
+ export { jwtEntityDef } from "./jwt";
5
+ export { organizationEntityDef } from "./organization";
6
+ export { passkeyEntityDef } from "./passkey";
2
7
  export { phoneNumberEntityDef } from "./phone-number";
8
+ export { ssoEntityDef } from "./sso";
3
9
  export { twoFactorEntityDef } from "./two-factor";
4
10
  export type { BetterAuthEntityDef, BetterAuthPluginId } from "./types";
5
11
  export { usernameEntityDef } from "./username";
6
12
 
7
13
  import { adminEntityDef } from "./admin";
14
+ import { anonymousEntityDef } from "./anonymous";
15
+ import { apiKeyEntityDef } from "./api-key";
16
+ import { jwtEntityDef } from "./jwt";
17
+ import { organizationEntityDef } from "./organization";
18
+ import { passkeyEntityDef } from "./passkey";
8
19
  import { phoneNumberEntityDef } from "./phone-number";
20
+ import { ssoEntityDef } from "./sso";
9
21
  import { twoFactorEntityDef } from "./two-factor";
10
22
  import type { BetterAuthEntityDef, BetterAuthPluginId } from "./types";
11
23
  import { usernameEntityDef } from "./username";
@@ -19,6 +31,12 @@ export const ENTITY_DEFINITIONS: Record<BetterAuthPluginId, BetterAuthEntityDef>
19
31
  username: usernameEntityDef,
20
32
  "phone-number": phoneNumberEntityDef,
21
33
  "2fa": twoFactorEntityDef,
34
+ sso: ssoEntityDef,
35
+ passkey: passkeyEntityDef,
36
+ organization: organizationEntityDef,
37
+ "api-key": apiKeyEntityDef,
38
+ jwt: jwtEntityDef,
39
+ anonymous: anonymousEntityDef,
22
40
  };
23
41
 
24
42
  /**
@@ -0,0 +1,35 @@
1
+ import type { BetterAuthEntityDef } from "./types";
2
+
3
+ /**
4
+ * better-auth JWT 플러그인 엔티티 정의
5
+ * https://www.better-auth.com/docs/plugins/jwt
6
+ *
7
+ * JWT 토큰 발급 및 JWKS 키 관리를 지원합니다.
8
+ */
9
+ export const jwtEntityDef: BetterAuthEntityDef = {
10
+ id: "jwt",
11
+ name: "JWT",
12
+ entities: [
13
+ {
14
+ id: "Jwks",
15
+ table: "jwks",
16
+ title: "JWKS",
17
+ props: [
18
+ { name: "id", type: "string", desc: "ID" },
19
+ { name: "public_key", type: "string", desc: "공개키" },
20
+ { name: "private_key", type: "string", desc: "비밀키" },
21
+ { name: "created_at", type: "date", dbDefault: "CURRENT_TIMESTAMP", desc: "생성일시" },
22
+ { name: "expires_at", type: "date", nullable: true, desc: "만료일시" },
23
+ ],
24
+ indexes: [],
25
+ subsets: {
26
+ A: ["id", "public_key", "private_key", "created_at", "expires_at"],
27
+ },
28
+ enums: {
29
+ JwksOrderBy: { "id-desc": "ID최신순", "created_at-desc": "생성일최신순" },
30
+ JwksSearchField: { id: "ID" },
31
+ },
32
+ },
33
+ ],
34
+ additionalProps: {},
35
+ };