sonamu 0.8.24 → 0.8.25

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 (61) hide show
  1. package/dist/api/__tests__/config.test.js +189 -0
  2. package/dist/api/config.d.ts.map +1 -1
  3. package/dist/api/config.js +7 -2
  4. package/dist/api/sonamu.d.ts.map +1 -1
  5. package/dist/api/sonamu.js +14 -10
  6. package/dist/auth/index.d.ts +1 -0
  7. package/dist/auth/index.d.ts.map +1 -1
  8. package/dist/auth/index.js +2 -1
  9. package/dist/auth/knex-adapter.d.ts +23 -0
  10. package/dist/auth/knex-adapter.d.ts.map +1 -0
  11. package/dist/auth/knex-adapter.js +159 -0
  12. package/dist/auth/plugins/wrappers/admin.d.ts +2 -2
  13. package/dist/bin/__tests__/ts-loader-register.test.js +45 -0
  14. package/dist/bin/cli.js +47 -9
  15. package/dist/bin/ts-loader-register.js +3 -29
  16. package/dist/bin/ts-loader-registration.d.ts +2 -0
  17. package/dist/bin/ts-loader-registration.d.ts.map +1 -0
  18. package/dist/bin/ts-loader-registration.js +42 -0
  19. package/package.json +4 -4
  20. package/src/api/__tests__/config.test.ts +225 -0
  21. package/src/api/config.ts +10 -4
  22. package/src/api/sonamu.ts +16 -13
  23. package/src/auth/index.ts +1 -0
  24. package/src/auth/knex-adapter.ts +207 -0
  25. package/src/bin/__tests__/ts-loader-register.test.ts +62 -0
  26. package/src/bin/cli.ts +52 -9
  27. package/src/bin/ts-loader-register.ts +2 -32
  28. package/src/bin/ts-loader-registration.ts +55 -0
  29. package/src/skills/commands/sonamu-skills.md +20 -0
  30. package/src/skills/sonamu/SKILL.md +136 -137
  31. package/src/skills/sonamu/ai-agents.md +69 -69
  32. package/src/skills/sonamu/api.md +147 -147
  33. package/src/skills/sonamu/auth-migration.md +220 -220
  34. package/src/skills/sonamu/auth-plugins.md +83 -83
  35. package/src/skills/sonamu/auth.md +106 -106
  36. package/src/skills/sonamu/cdd.md +65 -200
  37. package/src/skills/sonamu/cone.md +138 -138
  38. package/src/skills/sonamu/config.md +191 -191
  39. package/src/skills/sonamu/create-sonamu.md +66 -66
  40. package/src/skills/sonamu/database.md +158 -158
  41. package/src/skills/sonamu/entity-basic.md +292 -293
  42. package/src/skills/sonamu/entity-relations.md +246 -246
  43. package/src/skills/sonamu/entity-validation-checklist.md +124 -124
  44. package/src/skills/sonamu/fixture-cli.md +231 -231
  45. package/src/skills/sonamu/framework-change.md +37 -37
  46. package/src/skills/sonamu/frontend.md +223 -223
  47. package/src/skills/sonamu/i18n.md +82 -82
  48. package/src/skills/sonamu/migration.md +77 -77
  49. package/src/skills/sonamu/model.md +222 -222
  50. package/src/skills/sonamu/naite.md +86 -86
  51. package/src/skills/sonamu/project-init.md +228 -228
  52. package/src/skills/sonamu/puri.md +122 -122
  53. package/src/skills/sonamu/scaffolding.md +154 -154
  54. package/src/skills/sonamu/skill-contribution.md +124 -124
  55. package/src/skills/sonamu/subset.md +46 -46
  56. package/src/skills/sonamu/tasks.md +82 -82
  57. package/src/skills/sonamu/testing-devrunner.md +147 -147
  58. package/src/skills/sonamu/testing.md +673 -673
  59. package/src/skills/sonamu/upsert.md +79 -79
  60. package/src/skills/sonamu/vector.md +67 -67
  61. package/src/skills/sonamu/workflow.md +0 -317
@@ -1,34 +1,8 @@
1
- import { register } from "node:module";
2
- import * as path from "node:path";
3
- import { exists } from "../utils/fs-utils.js";
4
1
  import { findApiRootPath } from "../utils/utils.js";
2
+ import { ensureTsLoaderRegistered } from "./ts-loader-registration.js";
5
3
  /**
6
4
  * @sonamu-kit/ts-loader/loader를 등록하는 스크립트입니다.
7
5
  * 이 스크립트는 sonamu cli로 dev 실행할 때 --import로 실행됩니다.
8
- */ async function setupSwcConfig() {
9
- try {
10
- const apiRoot = findApiRootPath();
11
- // 프로젝트 루트에서 .swcrc 찾기
12
- const projectSwcrcPath = path.join(apiRoot, ".swcrc");
13
- if (await exists(projectSwcrcPath)) {
14
- // 사용자 프로젝트에 .swcrc가 있으면 우선으로 사용합니다.
15
- process.env.SWCRC_PATH = projectSwcrcPath;
16
- return;
17
- }
18
- // 아니라면 sonamu가 관리하는 .swcrc.project-default를 가져다 씁니다.
19
- const sonamuSwcrcPath = path.join(import.meta.dirname, "..", "..", ".swcrc.project-default");
20
- if (await exists(sonamuSwcrcPath)) {
21
- process.env.SWCRC_PATH = sonamuSwcrcPath;
22
- return;
23
- }
24
- } catch {
25
- // 환경 변수 설정 실패는 무시 (loader가 기본 설정 사용)
26
- }
27
- }
28
- // swc 설정 파일 경로를 환경 변수로 설정
29
- await setupSwcConfig();
30
- register("@sonamu-kit/ts-loader/loader", {
31
- parentURL: import.meta.url
32
- });
6
+ */ await ensureTsLoaderRegistered(findApiRootPath());
33
7
 
34
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9iaW4vdHMtbG9hZGVyLXJlZ2lzdGVyLnRzIl0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IHJlZ2lzdGVyIH0gZnJvbSBcIm5vZGU6bW9kdWxlXCI7XG5pbXBvcnQgKiBhcyBwYXRoIGZyb20gXCJub2RlOnBhdGhcIjtcbmltcG9ydCB7IGV4aXN0cyB9IGZyb20gXCIuLi91dGlscy9mcy11dGlscy5qc1wiO1xuaW1wb3J0IHsgZmluZEFwaVJvb3RQYXRoIH0gZnJvbSBcIi4uL3V0aWxzL3V0aWxzLmpzXCI7XG5cbi8qKlxuICogQHNvbmFtdS1raXQvdHMtbG9hZGVyL2xvYWRlcuulvCDrk7HroZ3tlZjripQg7Iqk7YGs66a97Yq47J6F64uI64ukLlxuICog7J20IOyKpO2BrOumve2KuOuKlCBzb25hbXUgY2xp66GcIGRldiDsi6TtlontlaAg65WMIC0taW1wb3J066GcIOyLpO2WieuQqeuLiOuLpC5cbiAqL1xuYXN5bmMgZnVuY3Rpb24gc2V0dXBTd2NDb25maWcoKSB7XG4gIHRyeSB7XG4gICAgY29uc3QgYXBpUm9vdCA9IGZpbmRBcGlSb290UGF0aCgpO1xuXG4gICAgLy8g7ZSE66Gc7KCd7Yq4IOujqO2KuOyXkOyEnCAuc3djcmMg7LC+6riwXG4gICAgY29uc3QgcHJvamVjdFN3Y3JjUGF0aCA9IHBhdGguam9pbihhcGlSb290LCBcIi5zd2NyY1wiKTtcbiAgICBpZiAoYXdhaXQgZXhpc3RzKHByb2plY3RTd2NyY1BhdGgpKSB7XG4gICAgICAvLyDsgqzsmqnsnpAg7ZSE66Gc7KCd7Yq47JeQIC5zd2NyY+qwgCDsnojsnLzrqbQg7Jqw7ISg7Jy866GcIOyCrOyaqe2VqeuLiOuLpC5cbiAgICAgIHByb2Nlc3MuZW52LlNXQ1JDX1BBVEggPSBwcm9qZWN0U3djcmNQYXRoO1xuICAgICAgcmV0dXJuO1xuICAgIH1cblxuICAgIC8vIOyVhOuLiOudvOuptCBzb25hbXXqsIAg6rSA66as7ZWY64qUIC5zd2NyYy5wcm9qZWN0LWRlZmF1bHTrpbwg6rCA7KC464ukIOyUgeuLiOuLpC5cbiAgICBjb25zdCBzb25hbXVTd2NyY1BhdGggPSBwYXRoLmpvaW4oaW1wb3J0Lm1ldGEuZGlybmFtZSwgXCIuLlwiLCBcIi4uXCIsIFwiLnN3Y3JjLnByb2plY3QtZGVmYXVsdFwiKTtcbiAgICBpZiAoYXdhaXQgZXhpc3RzKHNvbmFtdVN3Y3JjUGF0aCkpIHtcbiAgICAgIHByb2Nlc3MuZW52LlNXQ1JDX1BBVEggPSBzb25hbXVTd2NyY1BhdGg7XG4gICAgICByZXR1cm47XG4gICAgfVxuICB9IGNhdGNoIHtcbiAgICAvLyDtmZjqsr0g67OA7IiYIOyEpOyglSDsi6TtjKjripQg66y07IucIChsb2FkZXLqsIAg6riw67O4IOyEpOyglSDsgqzsmqkpXG4gIH1cbn1cblxuLy8gc3djIOyEpOyglSDtjIzsnbwg6rK966Gc66W8IO2ZmOqyvSDrs4DsiJjroZwg7ISk7KCVXG5hd2FpdCBzZXR1cFN3Y0NvbmZpZygpO1xuXG5yZWdpc3RlcihcIkBzb25hbXUta2l0L3RzLWxvYWRlci9sb2FkZXJcIiwge1xuICBwYXJlbnRVUkw6IGltcG9ydC5tZXRhLnVybCxcbn0pO1xuIl0sIm5hbWVzIjpbInJlZ2lzdGVyIiwicGF0aCIsImV4aXN0cyIsImZpbmRBcGlSb290UGF0aCIsInNldHVwU3djQ29uZmlnIiwiYXBpUm9vdCIsInByb2plY3RTd2NyY1BhdGgiLCJqb2luIiwicHJvY2VzcyIsImVudiIsIlNXQ1JDX1BBVEgiLCJzb25hbXVTd2NyY1BhdGgiLCJkaXJuYW1lIiwicGFyZW50VVJMIiwidXJsIl0sIm1hcHBpbmdzIjoiQUFBQSxTQUFTQSxRQUFRLFFBQVEsY0FBYztBQUN2QyxZQUFZQyxVQUFVLFlBQVk7QUFDbEMsU0FBU0MsTUFBTSxRQUFRLHVCQUF1QjtBQUM5QyxTQUFTQyxlQUFlLFFBQVEsb0JBQW9CO0FBRXBEOzs7Q0FHQyxHQUNELGVBQWVDO0lBQ2IsSUFBSTtRQUNGLE1BQU1DLFVBQVVGO1FBRWhCLHNCQUFzQjtRQUN0QixNQUFNRyxtQkFBbUJMLEtBQUtNLElBQUksQ0FBQ0YsU0FBUztRQUM1QyxJQUFJLE1BQU1ILE9BQU9JLG1CQUFtQjtZQUNsQyxvQ0FBb0M7WUFDcENFLFFBQVFDLEdBQUcsQ0FBQ0MsVUFBVSxHQUFHSjtZQUN6QjtRQUNGO1FBRUEscURBQXFEO1FBQ3JELE1BQU1LLGtCQUFrQlYsS0FBS00sSUFBSSxDQUFDLFlBQVlLLE9BQU8sRUFBRSxNQUFNLE1BQU07UUFDbkUsSUFBSSxNQUFNVixPQUFPUyxrQkFBa0I7WUFDakNILFFBQVFDLEdBQUcsQ0FBQ0MsVUFBVSxHQUFHQztZQUN6QjtRQUNGO0lBQ0YsRUFBRSxPQUFNO0lBQ04scUNBQXFDO0lBQ3ZDO0FBQ0Y7QUFFQSwwQkFBMEI7QUFDMUIsTUFBTVA7QUFFTkosU0FBUyxnQ0FBZ0M7SUFDdkNhLFdBQVcsWUFBWUMsR0FBRztBQUM1QiJ9
8
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9iaW4vdHMtbG9hZGVyLXJlZ2lzdGVyLnRzIl0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IGZpbmRBcGlSb290UGF0aCB9IGZyb20gXCIuLi91dGlscy91dGlscy5qc1wiO1xuaW1wb3J0IHsgZW5zdXJlVHNMb2FkZXJSZWdpc3RlcmVkIH0gZnJvbSBcIi4vdHMtbG9hZGVyLXJlZ2lzdHJhdGlvbi5qc1wiO1xuXG4vKipcbiAqIEBzb25hbXUta2l0L3RzLWxvYWRlci9sb2FkZXLrpbwg65Ox66Gd7ZWY64qUIOyKpO2BrOumve2KuOyeheuLiOuLpC5cbiAqIOydtCDsiqTtgazrpr3tirjripQgc29uYW11IGNsaeuhnCBkZXYg7Iuk7ZaJ7ZWgIOuVjCAtLWltcG9ydOuhnCDsi6TtlonrkKnri4jri6QuXG4gKi9cbmF3YWl0IGVuc3VyZVRzTG9hZGVyUmVnaXN0ZXJlZChmaW5kQXBpUm9vdFBhdGgoKSk7XG4iXSwibmFtZXMiOlsiZmluZEFwaVJvb3RQYXRoIiwiZW5zdXJlVHNMb2FkZXJSZWdpc3RlcmVkIl0sIm1hcHBpbmdzIjoiQUFBQSxTQUFTQSxlQUFlLFFBQVEsb0JBQW9CO0FBQ3BELFNBQVNDLHdCQUF3QixRQUFRLDhCQUE4QjtBQUV2RTs7O0NBR0MsR0FDRCxNQUFNQSx5QkFBeUJEIn0=
@@ -0,0 +1,2 @@
1
+ export declare function ensureTsLoaderRegistered(apiRoot: string): Promise<void>;
2
+ //# sourceMappingURL=ts-loader-registration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ts-loader-registration.d.ts","sourceRoot":"","sources":["../../src/bin/ts-loader-registration.ts"],"names":[],"mappings":"AA0CA,wBAAsB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAY7E"}
@@ -0,0 +1,42 @@
1
+ import { register } from "node:module";
2
+ import * as path from "node:path";
3
+ import { exists } from "../utils/fs-utils.js";
4
+ const tsLoaderRegisterStateKey = Symbol.for("sonamu.ts-loader-register.state");
5
+ function getTsLoaderRegisterState() {
6
+ const globalState = globalThis;
7
+ if (!globalState[tsLoaderRegisterStateKey]) {
8
+ globalState[tsLoaderRegisterStateKey] = {
9
+ registered: false
10
+ };
11
+ }
12
+ return globalState[tsLoaderRegisterStateKey];
13
+ }
14
+ async function setupSwcConfig(apiRoot) {
15
+ try {
16
+ const projectSwcrcPath = path.join(apiRoot, ".swcrc");
17
+ if (await exists(projectSwcrcPath)) {
18
+ process.env.SWCRC_PATH = projectSwcrcPath;
19
+ return;
20
+ }
21
+ const sonamuSwcrcPath = path.join(import.meta.dirname, "..", "..", ".swcrc.project-default");
22
+ if (await exists(sonamuSwcrcPath)) {
23
+ process.env.SWCRC_PATH = sonamuSwcrcPath;
24
+ return;
25
+ }
26
+ } catch {
27
+ // 환경 변수 설정 실패는 무시 (loader가 기본 설정 사용)
28
+ }
29
+ }
30
+ export async function ensureTsLoaderRegistered(apiRoot) {
31
+ const state = getTsLoaderRegisterState();
32
+ if (state.registered) {
33
+ return;
34
+ }
35
+ await setupSwcConfig(apiRoot);
36
+ register("@sonamu-kit/ts-loader/loader", {
37
+ parentURL: import.meta.url
38
+ });
39
+ state.registered = true;
40
+ }
41
+
42
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9iaW4vdHMtbG9hZGVyLXJlZ2lzdHJhdGlvbi50cyJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyByZWdpc3RlciB9IGZyb20gXCJub2RlOm1vZHVsZVwiO1xuaW1wb3J0ICogYXMgcGF0aCBmcm9tIFwibm9kZTpwYXRoXCI7XG5pbXBvcnQgeyBleGlzdHMgfSBmcm9tIFwiLi4vdXRpbHMvZnMtdXRpbHMuanNcIjtcblxuY29uc3QgdHNMb2FkZXJSZWdpc3RlclN0YXRlS2V5ID0gU3ltYm9sLmZvcihcInNvbmFtdS50cy1sb2FkZXItcmVnaXN0ZXIuc3RhdGVcIik7XG5cbnR5cGUgVHNMb2FkZXJSZWdpc3RlclN0YXRlID0ge1xuICByZWdpc3RlcmVkOiBib29sZWFuO1xufTtcblxudHlwZSBHbG9iYWxXaXRoVHNMb2FkZXJSZWdpc3RlclN0YXRlID0gdHlwZW9mIGdsb2JhbFRoaXMgJiB7XG4gIFt0c0xvYWRlclJlZ2lzdGVyU3RhdGVLZXldPzogVHNMb2FkZXJSZWdpc3RlclN0YXRlO1xufTtcblxuZnVuY3Rpb24gZ2V0VHNMb2FkZXJSZWdpc3RlclN0YXRlKCk6IFRzTG9hZGVyUmVnaXN0ZXJTdGF0ZSB7XG4gIGNvbnN0IGdsb2JhbFN0YXRlID0gZ2xvYmFsVGhpcyBhcyBHbG9iYWxXaXRoVHNMb2FkZXJSZWdpc3RlclN0YXRlO1xuXG4gIGlmICghZ2xvYmFsU3RhdGVbdHNMb2FkZXJSZWdpc3RlclN0YXRlS2V5XSkge1xuICAgIGdsb2JhbFN0YXRlW3RzTG9hZGVyUmVnaXN0ZXJTdGF0ZUtleV0gPSB7IHJlZ2lzdGVyZWQ6IGZhbHNlIH07XG4gIH1cblxuICByZXR1cm4gZ2xvYmFsU3RhdGVbdHNMb2FkZXJSZWdpc3RlclN0YXRlS2V5XTtcbn1cblxuYXN5bmMgZnVuY3Rpb24gc2V0dXBTd2NDb25maWcoYXBpUm9vdDogc3RyaW5nKSB7XG4gIHRyeSB7XG4gICAgY29uc3QgcHJvamVjdFN3Y3JjUGF0aCA9IHBhdGguam9pbihhcGlSb290LCBcIi5zd2NyY1wiKTtcbiAgICBpZiAoYXdhaXQgZXhpc3RzKHByb2plY3RTd2NyY1BhdGgpKSB7XG4gICAgICBwcm9jZXNzLmVudi5TV0NSQ19QQVRIID0gcHJvamVjdFN3Y3JjUGF0aDtcbiAgICAgIHJldHVybjtcbiAgICB9XG5cbiAgICBjb25zdCBzb25hbXVTd2NyY1BhdGggPSBwYXRoLmpvaW4oaW1wb3J0Lm1ldGEuZGlybmFtZSwgXCIuLlwiLCBcIi4uXCIsIFwiLnN3Y3JjLnByb2plY3QtZGVmYXVsdFwiKTtcbiAgICBpZiAoYXdhaXQgZXhpc3RzKHNvbmFtdVN3Y3JjUGF0aCkpIHtcbiAgICAgIHByb2Nlc3MuZW52LlNXQ1JDX1BBVEggPSBzb25hbXVTd2NyY1BhdGg7XG4gICAgICByZXR1cm47XG4gICAgfVxuICB9IGNhdGNoIHtcbiAgICAvLyDtmZjqsr0g67OA7IiYIOyEpOyglSDsi6TtjKjripQg66y07IucIChsb2FkZXLqsIAg6riw67O4IOyEpOyglSDsgqzsmqkpXG4gIH1cbn1cblxuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGVuc3VyZVRzTG9hZGVyUmVnaXN0ZXJlZChhcGlSb290OiBzdHJpbmcpOiBQcm9taXNlPHZvaWQ+IHtcbiAgY29uc3Qgc3RhdGUgPSBnZXRUc0xvYWRlclJlZ2lzdGVyU3RhdGUoKTtcbiAgaWYgKHN0YXRlLnJlZ2lzdGVyZWQpIHtcbiAgICByZXR1cm47XG4gIH1cblxuICBhd2FpdCBzZXR1cFN3Y0NvbmZpZyhhcGlSb290KTtcblxuICByZWdpc3RlcihcIkBzb25hbXUta2l0L3RzLWxvYWRlci9sb2FkZXJcIiwge1xuICAgIHBhcmVudFVSTDogaW1wb3J0Lm1ldGEudXJsLFxuICB9KTtcbiAgc3RhdGUucmVnaXN0ZXJlZCA9IHRydWU7XG59XG4iXSwibmFtZXMiOlsicmVnaXN0ZXIiLCJwYXRoIiwiZXhpc3RzIiwidHNMb2FkZXJSZWdpc3RlclN0YXRlS2V5IiwiU3ltYm9sIiwiZm9yIiwiZ2V0VHNMb2FkZXJSZWdpc3RlclN0YXRlIiwiZ2xvYmFsU3RhdGUiLCJnbG9iYWxUaGlzIiwicmVnaXN0ZXJlZCIsInNldHVwU3djQ29uZmlnIiwiYXBpUm9vdCIsInByb2plY3RTd2NyY1BhdGgiLCJqb2luIiwicHJvY2VzcyIsImVudiIsIlNXQ1JDX1BBVEgiLCJzb25hbXVTd2NyY1BhdGgiLCJkaXJuYW1lIiwiZW5zdXJlVHNMb2FkZXJSZWdpc3RlcmVkIiwic3RhdGUiLCJwYXJlbnRVUkwiLCJ1cmwiXSwibWFwcGluZ3MiOiJBQUFBLFNBQVNBLFFBQVEsUUFBUSxjQUFjO0FBQ3ZDLFlBQVlDLFVBQVUsWUFBWTtBQUNsQyxTQUFTQyxNQUFNLFFBQVEsdUJBQXVCO0FBRTlDLE1BQU1DLDJCQUEyQkMsT0FBT0MsR0FBRyxDQUFDO0FBVTVDLFNBQVNDO0lBQ1AsTUFBTUMsY0FBY0M7SUFFcEIsSUFBSSxDQUFDRCxXQUFXLENBQUNKLHlCQUF5QixFQUFFO1FBQzFDSSxXQUFXLENBQUNKLHlCQUF5QixHQUFHO1lBQUVNLFlBQVk7UUFBTTtJQUM5RDtJQUVBLE9BQU9GLFdBQVcsQ0FBQ0oseUJBQXlCO0FBQzlDO0FBRUEsZUFBZU8sZUFBZUMsT0FBZTtJQUMzQyxJQUFJO1FBQ0YsTUFBTUMsbUJBQW1CWCxLQUFLWSxJQUFJLENBQUNGLFNBQVM7UUFDNUMsSUFBSSxNQUFNVCxPQUFPVSxtQkFBbUI7WUFDbENFLFFBQVFDLEdBQUcsQ0FBQ0MsVUFBVSxHQUFHSjtZQUN6QjtRQUNGO1FBRUEsTUFBTUssa0JBQWtCaEIsS0FBS1ksSUFBSSxDQUFDLFlBQVlLLE9BQU8sRUFBRSxNQUFNLE1BQU07UUFDbkUsSUFBSSxNQUFNaEIsT0FBT2Usa0JBQWtCO1lBQ2pDSCxRQUFRQyxHQUFHLENBQUNDLFVBQVUsR0FBR0M7WUFDekI7UUFDRjtJQUNGLEVBQUUsT0FBTTtJQUNOLHFDQUFxQztJQUN2QztBQUNGO0FBRUEsT0FBTyxlQUFlRSx5QkFBeUJSLE9BQWU7SUFDNUQsTUFBTVMsUUFBUWQ7SUFDZCxJQUFJYyxNQUFNWCxVQUFVLEVBQUU7UUFDcEI7SUFDRjtJQUVBLE1BQU1DLGVBQWVDO0lBRXJCWCxTQUFTLGdDQUFnQztRQUN2Q3FCLFdBQVcsWUFBWUMsR0FBRztJQUM1QjtJQUNBRixNQUFNWCxVQUFVLEdBQUc7QUFDckIifQ==
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonamu",
3
- "version": "0.8.24",
3
+ "version": "0.8.25",
4
4
  "description": "Sonamu — TypeScript Fullstack API Framework",
5
5
  "keywords": [
6
6
  "typescript",
@@ -123,10 +123,10 @@
123
123
  "tsicli": "^1.0.5",
124
124
  "vite": "7.3.0",
125
125
  "vitest": "^4.0.10",
126
- "@sonamu-kit/hmr-runner": "^0.1.1",
127
126
  "@sonamu-kit/hmr-hook": "^0.4.1",
128
- "@sonamu-kit/ts-loader": "^2.1.3",
129
- "@sonamu-kit/tasks": "^0.2.0"
127
+ "@sonamu-kit/tasks": "^0.2.0",
128
+ "@sonamu-kit/hmr-runner": "^0.1.1",
129
+ "@sonamu-kit/ts-loader": "^2.1.3"
130
130
  },
131
131
  "devDependencies": {
132
132
  "@biomejs/biome": "^2.3.13",
@@ -0,0 +1,225 @@
1
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+
7
+ const tsLoaderRegisterStateKey = Symbol.for("sonamu.ts-loader-register.state");
8
+
9
+ type TsLoaderRegisterState = {
10
+ registered: boolean;
11
+ };
12
+
13
+ type GlobalWithTsLoaderRegisterState = typeof globalThis & {
14
+ [tsLoaderRegisterStateKey]?: TsLoaderRegisterState;
15
+ };
16
+
17
+ function resetRegisterState() {
18
+ const globalState = globalThis as GlobalWithTsLoaderRegisterState;
19
+ delete globalState[tsLoaderRegisterStateKey];
20
+ delete process.env.SWCRC_PATH;
21
+ }
22
+
23
+ async function createTempRoot(): Promise<string> {
24
+ return mkdtemp(path.join(os.tmpdir(), "sonamu-config-test-"));
25
+ }
26
+
27
+ async function writeSourceFixture(rootPath: string): Promise<void> {
28
+ await mkdir(path.join(rootPath, "src"), { recursive: true });
29
+ await writeFile(
30
+ path.join(rootPath, "src", "support.ts"),
31
+ `
32
+ export const fastifyOptions = { keepAliveTimeout: 4321 };
33
+ export function customPlugin() {
34
+ return "plugin";
35
+ }
36
+ export function contextProvider(defaultContext) {
37
+ return defaultContext;
38
+ }
39
+ export function guardHandler() {}
40
+ `,
41
+ );
42
+ await writeFile(
43
+ path.join(rootPath, "src", "sonamu.config.ts"),
44
+ `
45
+ import { customPlugin, contextProvider, fastifyOptions, guardHandler } from "./support";
46
+
47
+ export default {
48
+ api: {
49
+ dir: "./src",
50
+ route: {
51
+ prefix: "/api",
52
+ },
53
+ },
54
+ i18n: {
55
+ defaultLocale: "ko",
56
+ supportedLocales: ["ko"],
57
+ },
58
+ sync: {
59
+ targets: ["web"],
60
+ },
61
+ server: {
62
+ fastify: fastifyOptions,
63
+ plugins: {
64
+ custom: customPlugin,
65
+ },
66
+ apiConfig: {
67
+ contextProvider,
68
+ guardHandler,
69
+ },
70
+ },
71
+ test: {
72
+ parallel: true,
73
+ maxWorkers: 3,
74
+ devRunner: {
75
+ enabled: true,
76
+ routePrefix: "/__test__",
77
+ },
78
+ },
79
+ };
80
+ `,
81
+ );
82
+ }
83
+
84
+ async function writeDistFixture(rootPath: string): Promise<void> {
85
+ await mkdir(path.join(rootPath, "dist"), { recursive: true });
86
+ await writeFile(
87
+ path.join(rootPath, "dist", "sonamu.config.js"),
88
+ `
89
+ export default {
90
+ api: {
91
+ dir: "./dist",
92
+ route: {
93
+ prefix: "/api",
94
+ },
95
+ },
96
+ i18n: {
97
+ defaultLocale: "en",
98
+ supportedLocales: ["en"],
99
+ },
100
+ sync: {
101
+ targets: ["web"],
102
+ },
103
+ server: {
104
+ apiConfig: {
105
+ contextProvider(defaultContext) {
106
+ return defaultContext;
107
+ },
108
+ guardHandler() {},
109
+ },
110
+ },
111
+ test: {
112
+ devRunner: {
113
+ enabled: false,
114
+ },
115
+ },
116
+ };
117
+ `,
118
+ );
119
+ }
120
+
121
+ describe("loadConfig", () => {
122
+ const tempRoots: string[] = [];
123
+ const originalHot = process.env.HOT;
124
+ const originalVitest = process.env.VITEST;
125
+ const originalSwcrcPath = process.env.SWCRC_PATH;
126
+
127
+ beforeEach(() => {
128
+ vi.resetModules();
129
+ vi.restoreAllMocks();
130
+ vi.unmock("../../bin/ts-loader-registration");
131
+ resetRegisterState();
132
+ });
133
+
134
+ afterEach(async () => {
135
+ vi.resetModules();
136
+ vi.restoreAllMocks();
137
+ vi.unmock("../../bin/ts-loader-registration");
138
+ resetRegisterState();
139
+
140
+ if (originalHot === undefined) {
141
+ delete process.env.HOT;
142
+ } else {
143
+ process.env.HOT = originalHot;
144
+ }
145
+
146
+ if (originalVitest === undefined) {
147
+ delete process.env.VITEST;
148
+ } else {
149
+ process.env.VITEST = originalVitest;
150
+ }
151
+
152
+ if (originalSwcrcPath === undefined) {
153
+ delete process.env.SWCRC_PATH;
154
+ } else {
155
+ process.env.SWCRC_PATH = originalSwcrcPath;
156
+ }
157
+
158
+ await Promise.all(
159
+ tempRoots.splice(0).map((rootPath) => rm(rootPath, { recursive: true, force: true })),
160
+ );
161
+ });
162
+
163
+ it("source config 로드 전에 ts-loader 등록을 보장한다", async () => {
164
+ const rootPath = await createTempRoot();
165
+ tempRoots.push(rootPath);
166
+ await writeSourceFixture(rootPath);
167
+
168
+ process.env.VITEST = "true";
169
+ const ensureTsLoaderRegistered = vi.fn(async () => {});
170
+ vi.doMock("../../bin/ts-loader-registration", () => ({
171
+ ensureTsLoaderRegistered,
172
+ }));
173
+
174
+ const { loadConfig } = await import("../config");
175
+ const config = await loadConfig(rootPath);
176
+
177
+ expect(ensureTsLoaderRegistered).toHaveBeenCalledTimes(1);
178
+ expect(ensureTsLoaderRegistered).toHaveBeenCalledWith(rootPath);
179
+ expect(config.test?.devRunner?.enabled).toBe(true);
180
+ });
181
+
182
+ it("dist config 로딩 경로는 기존과 동일하게 유지한다", async () => {
183
+ const rootPath = await createTempRoot();
184
+ tempRoots.push(rootPath);
185
+ await writeDistFixture(rootPath);
186
+
187
+ delete process.env.HOT;
188
+ delete process.env.VITEST;
189
+ const ensureTsLoaderRegistered = vi.fn(async () => {});
190
+ vi.doMock("../../bin/ts-loader-registration", () => ({
191
+ ensureTsLoaderRegistered,
192
+ }));
193
+
194
+ const { loadConfig } = await import("../config");
195
+ const config = await loadConfig(rootPath);
196
+
197
+ expect(ensureTsLoaderRegistered).not.toHaveBeenCalled();
198
+ expect(config.api.dir).toBe("./dist");
199
+ expect(config.test?.devRunner?.enabled).toBe(false);
200
+ });
201
+
202
+ it("source config가 확장자 없는 상대 import와 런타임 객체를 유지하며 반복 로드된다", async () => {
203
+ const rootPath = await createTempRoot();
204
+ tempRoots.push(rootPath);
205
+ await writeSourceFixture(rootPath);
206
+
207
+ process.env.VITEST = "true";
208
+ vi.unmock("../../bin/ts-loader-registration");
209
+ const { loadConfig } = await import("../config");
210
+ const supportModule = await import(
211
+ pathToFileURL(path.join(rootPath, "src", "support.ts")).href
212
+ );
213
+
214
+ const firstConfig = await loadConfig(rootPath);
215
+ const secondConfig = await loadConfig(rootPath);
216
+
217
+ expect(firstConfig.server.fastify).toBe(supportModule.fastifyOptions);
218
+ expect(firstConfig.server.plugins?.custom).toBe(supportModule.customPlugin);
219
+ expect(firstConfig.test?.parallel).toBe(true);
220
+ expect(firstConfig.test?.maxWorkers).toBe(3);
221
+ expect(firstConfig.test?.devRunner?.enabled).toBe(true);
222
+ expect(secondConfig.server.plugins?.custom).toBe(supportModule.customPlugin);
223
+ expect(secondConfig.server.fastify).toBe(supportModule.fastifyOptions);
224
+ });
225
+ });
package/src/api/config.ts CHANGED
@@ -289,10 +289,16 @@ export function defineConfig(config: Executable<SonamuConfig>): Promise<SonamuCo
289
289
  */
290
290
  export async function loadConfig(rootPath: string): Promise<SonamuConfig> {
291
291
  const start = performance.now();
292
- const configPath =
293
- process.env.HOT === "yes" || process.env.VITEST === "true"
294
- ? `${rootPath}/src/sonamu.config.ts`
295
- : `${rootPath}/dist/sonamu.config.js`;
292
+ const shouldLoadSourceConfig = process.env.HOT === "yes" || process.env.VITEST === "true";
293
+ const configPath = shouldLoadSourceConfig
294
+ ? `${rootPath}/src/sonamu.config.ts`
295
+ : `${rootPath}/dist/sonamu.config.js`;
296
+
297
+ if (shouldLoadSourceConfig) {
298
+ const { ensureTsLoaderRegistered } = await import("../bin/ts-loader-registration");
299
+ await ensureTsLoaderRegistered(rootPath);
300
+ }
301
+
296
302
  const { default: config } = await import(`file://${configPath}`);
297
303
  const importTime = performance.now() - start;
298
304
  process.env.NODE_ENV !== "test" &&
package/src/api/sonamu.ts CHANGED
@@ -9,7 +9,6 @@ import type { IncomingMessage, Server, ServerResponse } from "http";
9
9
  import mime, { lookup as mimeLookup } from "mime-types";
10
10
  import os from "os";
11
11
  import path from "path";
12
- import type { PoolConfig } from "pg";
13
12
  import type { ZodObject } from "zod";
14
13
  import {
15
14
  BASE_FIELD_MAPPINGS,
@@ -232,6 +231,22 @@ class SonamuClass {
232
231
  // Cache 초기화
233
232
  await this.initializeCache(this.config.server.cache, forTesting);
234
233
 
234
+ // BetterAuth 초기화
235
+ const authConfig = this.config.server.auth;
236
+ if (authConfig) {
237
+ // 사용자 설정과 기본값을 merge
238
+ const mergedFieldMappings = merge(BASE_FIELD_MAPPINGS, authConfig);
239
+
240
+ // better-auth 인스턴스 생성
241
+ const { betterAuth } = await import("better-auth");
242
+ const { sonamuKnexAdapter } = await import("../auth/knex-adapter");
243
+
244
+ this._auth = betterAuth({
245
+ database: sonamuKnexAdapter(),
246
+ ...mergedFieldMappings,
247
+ });
248
+ }
249
+
235
250
  // 테스팅인 경우 싱크 없이 중단
236
251
  if (forTesting) {
237
252
  this.isInitialized = true;
@@ -1243,18 +1258,6 @@ class SonamuClass {
1243
1258
 
1244
1259
  const basePath = options.basePath ?? "/api/auth";
1245
1260
 
1246
- // 사용자 설정과 기본값을 merge
1247
- const mergedFieldMappings = merge(BASE_FIELD_MAPPINGS, options);
1248
-
1249
- // better-auth 인스턴스 생성
1250
- const { betterAuth } = await import("better-auth");
1251
- const { Pool } = await import("pg");
1252
-
1253
- this._auth = betterAuth({
1254
- database: new Pool(DB.getDBConfig("w").connection as PoolConfig),
1255
- ...mergedFieldMappings,
1256
- });
1257
-
1258
1261
  // better-auth 라우트 등록
1259
1262
  server.route({
1260
1263
  method: ["GET", "POST"],
package/src/auth/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export type { GenerateBetterAuthEntitiesOptions } from "./auth-generator";
2
2
  export { generateBetterAuthEntities } from "./auth-generator";
3
3
  export { BASE_FIELD_MAPPINGS, betterAuthV1 } from "./better-auth-entities";
4
+ export { sonamuKnexAdapter } from "./knex-adapter";
4
5
 
5
6
  // 외부로는 wrappers만 export (admin, twoFactor 등 래퍼 함수와 SCHEMA)
6
7
  export * from "./plugins";
@@ -0,0 +1,207 @@
1
+ import type { BetterAuthOptions } from "better-auth";
2
+ import type {
3
+ AdapterFactoryCustomizeAdapterCreator,
4
+ DBTransactionAdapter,
5
+ } from "better-auth/adapters";
6
+ import { createAdapterFactory } from "better-auth/adapters";
7
+ import type { Knex } from "knex";
8
+ import { DB } from "../database/db";
9
+
10
+ interface CleanedWhere {
11
+ field: string;
12
+ value: string | number | boolean | string[] | number[] | Date | null;
13
+ operator: string;
14
+ connector: string;
15
+ }
16
+
17
+ /**
18
+ * better-auth용 Sonamu knex 어댑터
19
+ *
20
+ * better-auth의 모든 쿼리를 DB.getDB()를 통해 실행하여
21
+ * Sonamu 테스트 트랜잭션과 동일한 커넥션을 공유합니다.
22
+ */
23
+ export const sonamuKnexAdapter = () => {
24
+ let lazyOptions: BetterAuthOptions | null = null;
25
+
26
+ const createCustomAdapter = (
27
+ getDb: () => Knex | Knex.Transaction,
28
+ ): AdapterFactoryCustomizeAdapterCreator => {
29
+ return () => ({
30
+ create: async ({ model, data }) => {
31
+ const [row] = await getDb()(model).insert(data).returning("*");
32
+ return row;
33
+ },
34
+
35
+ findOne: async ({ model, where }) => {
36
+ let query = getDb()(model);
37
+ query = applyWhere(query, where);
38
+ const row = await query.first();
39
+ return row ?? null;
40
+ },
41
+
42
+ findMany: async ({ model, where, limit, offset, sortBy }) => {
43
+ let query = getDb()(model);
44
+ if (where) {
45
+ query = applyWhere(query, where);
46
+ }
47
+ if (sortBy) {
48
+ query = query.orderBy(sortBy.field, sortBy.direction);
49
+ }
50
+ if (limit) {
51
+ query = query.limit(limit);
52
+ }
53
+ if (offset) {
54
+ query = query.offset(offset);
55
+ }
56
+ return await query;
57
+ },
58
+
59
+ update: async ({ model, where, update }) => {
60
+ let query = getDb()(model);
61
+ query = applyWhere(query, where);
62
+ const [row] = await query.update(update).returning("*");
63
+ return row ?? null;
64
+ },
65
+
66
+ updateMany: async ({ model, where, update }) => {
67
+ let query = getDb()(model);
68
+ query = applyWhere(query, where);
69
+ const count = await query.update(update);
70
+ return count;
71
+ },
72
+
73
+ delete: async ({ model, where }) => {
74
+ let query = getDb()(model);
75
+ query = applyWhere(query, where);
76
+ await query.del();
77
+ },
78
+
79
+ deleteMany: async ({ model, where }) => {
80
+ let query = getDb()(model);
81
+ query = applyWhere(query, where);
82
+ const count = await query.del();
83
+ return count;
84
+ },
85
+
86
+ count: async ({ model, where }) => {
87
+ let query = getDb()(model);
88
+ if (where) {
89
+ query = applyWhere(query, where);
90
+ }
91
+ const [{ count }] = await query.count("* as count");
92
+ return Number(count);
93
+ },
94
+ });
95
+ };
96
+
97
+ const adapterConfig = {
98
+ adapterId: "sonamu-knex",
99
+ adapterName: "Sonamu Knex Adapter",
100
+ usePlural: false,
101
+ supportsJSON: true,
102
+ supportsDates: true,
103
+ supportsBooleans: true,
104
+ supportsNumericIds: false,
105
+ transaction: async <R>(cb: (trx: DBTransactionAdapter) => Promise<R>): Promise<R> => {
106
+ const db = DB.getDB("w");
107
+ return db.transaction(async (trx) => {
108
+ const options = lazyOptions;
109
+ if (!options) {
110
+ throw new Error("sonamuKnexAdapter: options not initialized");
111
+ }
112
+ return cb(
113
+ createAdapterFactory({
114
+ config: adapterConfig,
115
+ adapter: createCustomAdapter(() => trx),
116
+ })(options),
117
+ );
118
+ });
119
+ },
120
+ };
121
+
122
+ const adapterCreator = createAdapterFactory({
123
+ config: adapterConfig,
124
+ adapter: createCustomAdapter(() => DB.getDB("w")),
125
+ });
126
+
127
+ return (options: BetterAuthOptions) => {
128
+ lazyOptions = options;
129
+ return adapterCreator(options);
130
+ };
131
+ };
132
+
133
+ /**
134
+ * Better Auth의 공식 어댑터(Kysely, Drizzle, Prisma, MongoDB) 패턴에 맞춰
135
+ * AND 그룹과 OR 그룹을 분리한 뒤 top-level AND로 결합합니다.
136
+ * 결과: (A AND B AND ...) AND (C OR D OR ...)
137
+ */
138
+ export function applyWhere(
139
+ query: Knex.QueryBuilder,
140
+ conditions: CleanedWhere[],
141
+ ): Knex.QueryBuilder {
142
+ const andGroup = conditions.filter((c) => c.connector !== "OR");
143
+ const orGroup = conditions.filter((c) => c.connector === "OR");
144
+
145
+ if (andGroup.length > 0) {
146
+ for (const condition of andGroup) {
147
+ query = applyCondition(query, condition, "where");
148
+ }
149
+ }
150
+
151
+ if (orGroup.length > 0) {
152
+ query = query.where(function (this: Knex.QueryBuilder) {
153
+ for (let i = 0; i < orGroup.length; i++) {
154
+ applyCondition(this, orGroup[i], i === 0 ? "where" : "orWhere");
155
+ }
156
+ });
157
+ }
158
+
159
+ return query;
160
+ }
161
+
162
+ function applyCondition(
163
+ query: Knex.QueryBuilder,
164
+ condition: CleanedWhere,
165
+ method: "where" | "orWhere",
166
+ ): Knex.QueryBuilder {
167
+ const { field, value, operator } = condition;
168
+
169
+ switch (operator) {
170
+ case "eq":
171
+ if (value === null) {
172
+ return query[method === "orWhere" ? "orWhereNull" : "whereNull"](field);
173
+ }
174
+ return query[method](field, "=", value);
175
+ case "ne":
176
+ if (value === null) {
177
+ return query[method === "orWhere" ? "orWhereNotNull" : "whereNotNull"](field);
178
+ }
179
+ return query[method](field, "!=", value);
180
+ case "lt":
181
+ return query[method](field, "<", value);
182
+ case "lte":
183
+ return query[method](field, "<=", value);
184
+ case "gt":
185
+ return query[method](field, ">", value);
186
+ case "gte":
187
+ return query[method](field, ">=", value);
188
+ case "in":
189
+ return query[method === "orWhere" ? "orWhereIn" : "whereIn"](
190
+ field,
191
+ value as (string | number)[],
192
+ );
193
+ case "not_in":
194
+ return query[method === "orWhere" ? "orWhereNotIn" : "whereNotIn"](
195
+ field,
196
+ value as (string | number)[],
197
+ );
198
+ case "contains":
199
+ return query[method](field, "like", `%${value}%`);
200
+ case "starts_with":
201
+ return query[method](field, "like", `${value}%`);
202
+ case "ends_with":
203
+ return query[method](field, "like", `%${value}`);
204
+ default:
205
+ return query;
206
+ }
207
+ }