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.
- package/dist/api/__tests__/config.test.js +189 -0
- package/dist/api/config.d.ts.map +1 -1
- package/dist/api/config.js +7 -2
- package/dist/api/sonamu.d.ts.map +1 -1
- package/dist/api/sonamu.js +14 -10
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +2 -1
- package/dist/auth/knex-adapter.d.ts +23 -0
- package/dist/auth/knex-adapter.d.ts.map +1 -0
- package/dist/auth/knex-adapter.js +159 -0
- package/dist/auth/plugins/wrappers/admin.d.ts +2 -2
- package/dist/bin/__tests__/ts-loader-register.test.js +45 -0
- package/dist/bin/cli.js +47 -9
- package/dist/bin/ts-loader-register.js +3 -29
- package/dist/bin/ts-loader-registration.d.ts +2 -0
- package/dist/bin/ts-loader-registration.d.ts.map +1 -0
- package/dist/bin/ts-loader-registration.js +42 -0
- package/package.json +4 -4
- package/src/api/__tests__/config.test.ts +225 -0
- package/src/api/config.ts +10 -4
- package/src/api/sonamu.ts +16 -13
- package/src/auth/index.ts +1 -0
- package/src/auth/knex-adapter.ts +207 -0
- package/src/bin/__tests__/ts-loader-register.test.ts +62 -0
- package/src/bin/cli.ts +52 -9
- package/src/bin/ts-loader-register.ts +2 -32
- package/src/bin/ts-loader-registration.ts +55 -0
- package/src/skills/commands/sonamu-skills.md +20 -0
- package/src/skills/sonamu/SKILL.md +136 -137
- package/src/skills/sonamu/ai-agents.md +69 -69
- package/src/skills/sonamu/api.md +147 -147
- package/src/skills/sonamu/auth-migration.md +220 -220
- package/src/skills/sonamu/auth-plugins.md +83 -83
- package/src/skills/sonamu/auth.md +106 -106
- package/src/skills/sonamu/cdd.md +65 -200
- package/src/skills/sonamu/cone.md +138 -138
- package/src/skills/sonamu/config.md +191 -191
- package/src/skills/sonamu/create-sonamu.md +66 -66
- package/src/skills/sonamu/database.md +158 -158
- package/src/skills/sonamu/entity-basic.md +292 -293
- package/src/skills/sonamu/entity-relations.md +246 -246
- package/src/skills/sonamu/entity-validation-checklist.md +124 -124
- package/src/skills/sonamu/fixture-cli.md +231 -231
- package/src/skills/sonamu/framework-change.md +37 -37
- package/src/skills/sonamu/frontend.md +223 -223
- package/src/skills/sonamu/i18n.md +82 -82
- package/src/skills/sonamu/migration.md +77 -77
- package/src/skills/sonamu/model.md +222 -222
- package/src/skills/sonamu/naite.md +86 -86
- package/src/skills/sonamu/project-init.md +228 -228
- package/src/skills/sonamu/puri.md +122 -122
- package/src/skills/sonamu/scaffolding.md +154 -154
- package/src/skills/sonamu/skill-contribution.md +124 -124
- package/src/skills/sonamu/subset.md +46 -46
- package/src/skills/sonamu/tasks.md +82 -82
- package/src/skills/sonamu/testing-devrunner.md +147 -147
- package/src/skills/sonamu/testing.md +673 -673
- package/src/skills/sonamu/upsert.md +79 -79
- package/src/skills/sonamu/vector.md +67 -67
- 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
|
-
*/
|
|
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,
|
|
8
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9iaW4vdHMtbG9hZGVyLXJlZ2lzdGVyLnRzIl0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IGZpbmRBcGlSb290UGF0aCB9IGZyb20gXCIuLi91dGlscy91dGlscy5qc1wiO1xuaW1wb3J0IHsgZW5zdXJlVHNMb2FkZXJSZWdpc3RlcmVkIH0gZnJvbSBcIi4vdHMtbG9hZGVyLXJlZ2lzdHJhdGlvbi5qc1wiO1xuXG4vKipcbiAqIEBzb25hbXUta2l0L3RzLWxvYWRlci9sb2FkZXLrpbwg65Ox66Gd7ZWY64qUIOyKpO2BrOumve2KuOyeheuLiOuLpC5cbiAqIOydtCDsiqTtgazrpr3tirjripQgc29uYW11IGNsaeuhnCBkZXYg7Iuk7ZaJ7ZWgIOuVjCAtLWltcG9ydOuhnCDsi6TtlonrkKnri4jri6QuXG4gKi9cbmF3YWl0IGVuc3VyZVRzTG9hZGVyUmVnaXN0ZXJlZChmaW5kQXBpUm9vdFBhdGgoKSk7XG4iXSwibmFtZXMiOlsiZmluZEFwaVJvb3RQYXRoIiwiZW5zdXJlVHNMb2FkZXJSZWdpc3RlcmVkIl0sIm1hcHBpbmdzIjoiQUFBQSxTQUFTQSxlQUFlLFFBQVEsb0JBQW9CO0FBQ3BELFNBQVNDLHdCQUF3QixRQUFRLDhCQUE4QjtBQUV2RTs7O0NBR0MsR0FDRCxNQUFNQSx5QkFBeUJEIn0=
|
|
@@ -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.
|
|
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/
|
|
129
|
-
"@sonamu-kit/
|
|
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
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
+
}
|