sonamu 0.8.24 → 0.8.26
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 +163 -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/dist/cone/cone-generator.js +3 -3
- package/dist/database/puri-subset.test-d.js +9 -1
- package/dist/database/puri-subset.types.d.ts +1 -1
- package/dist/database/puri-subset.types.d.ts.map +1 -1
- package/dist/database/puri-subset.types.js +1 -1
- package/dist/testing/fixture-generator.js +5 -5
- package/dist/ui/ai-client.js +2 -2
- package/dist/ui/api.d.ts.map +1 -1
- package/dist/ui/api.js +14 -14
- package/dist/ui/cdd-service.d.ts +15 -18
- package/dist/ui/cdd-service.d.ts.map +1 -1
- package/dist/ui/cdd-service.js +246 -222
- package/dist/ui/cdd-types.d.ts +41 -68
- package/dist/ui/cdd-types.d.ts.map +1 -1
- package/dist/ui/cdd-types.js +2 -2
- package/dist/ui-web/assets/index-CKo0Z2Iu.css +1 -0
- package/dist/ui-web/assets/{index-CxiydzeC.js → index-DK-2aacv.js} +83 -83
- package/dist/ui-web/index.html +2 -2
- package/package.json +6 -2
- 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 +208 -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/cone/cone-generator.ts +2 -2
- package/src/database/puri-subset.test-d.ts +102 -0
- package/src/database/puri-subset.types.ts +1 -1
- package/src/skills/commands/sonamu-skills.md +20 -0
- package/src/skills/sonamu/SKILL.md +179 -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/testing/fixture-generator.ts +4 -4
- package/src/ui/ai-client.ts +1 -1
- package/src/ui/api.ts +18 -17
- package/src/ui/cdd-service.ts +264 -254
- package/src/ui/cdd-types.ts +40 -75
- package/dist/ui-web/assets/index-BrQKU3j9.css +0 -1
- package/src/skills/sonamu/workflow.md +0 -317
package/dist/ui-web/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="/sonamu-ui/setting.svg" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>{{projectName}}: Sonamu UI</title>
|
|
8
|
-
<script type="module" crossorigin src="/sonamu-ui/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/sonamu-ui/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/sonamu-ui/assets/index-DK-2aacv.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/sonamu-ui/assets/index-CKo0Z2Iu.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sonamu",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.26",
|
|
4
4
|
"description": "Sonamu — TypeScript Fullstack API Framework",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -55,6 +55,10 @@
|
|
|
55
55
|
"import": "./dist/filter/index.js",
|
|
56
56
|
"types": "./dist/filter/index.d.ts"
|
|
57
57
|
},
|
|
58
|
+
"./auth/plugins": {
|
|
59
|
+
"import": "./dist/auth/plugins/index.js",
|
|
60
|
+
"types": "./dist/auth/plugins/index.d.ts"
|
|
61
|
+
},
|
|
58
62
|
"./cdd-types": {
|
|
59
63
|
"import": "./dist/ui/cdd-types.js",
|
|
60
64
|
"types": "./dist/ui/cdd-types.d.ts"
|
|
@@ -123,8 +127,8 @@
|
|
|
123
127
|
"tsicli": "^1.0.5",
|
|
124
128
|
"vite": "7.3.0",
|
|
125
129
|
"vitest": "^4.0.10",
|
|
126
|
-
"@sonamu-kit/hmr-runner": "^0.1.1",
|
|
127
130
|
"@sonamu-kit/hmr-hook": "^0.4.1",
|
|
131
|
+
"@sonamu-kit/hmr-runner": "^0.1.1",
|
|
128
132
|
"@sonamu-kit/ts-loader": "^2.1.3",
|
|
129
133
|
"@sonamu-kit/tasks": "^0.2.0"
|
|
130
134
|
},
|
|
@@ -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,208 @@
|
|
|
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 ({ getFieldName }) => ({
|
|
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
|
+
const dbField = getFieldName({ model, field: sortBy.field });
|
|
49
|
+
query = query.orderBy(dbField, sortBy.direction);
|
|
50
|
+
}
|
|
51
|
+
if (limit) {
|
|
52
|
+
query = query.limit(limit);
|
|
53
|
+
}
|
|
54
|
+
if (offset) {
|
|
55
|
+
query = query.offset(offset);
|
|
56
|
+
}
|
|
57
|
+
return await query;
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
update: async ({ model, where, update }) => {
|
|
61
|
+
let query = getDb()(model);
|
|
62
|
+
query = applyWhere(query, where);
|
|
63
|
+
const [row] = await query.update(update).returning("*");
|
|
64
|
+
return row ?? null;
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
updateMany: async ({ model, where, update }) => {
|
|
68
|
+
let query = getDb()(model);
|
|
69
|
+
query = applyWhere(query, where);
|
|
70
|
+
const count = await query.update(update);
|
|
71
|
+
return count;
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
delete: async ({ model, where }) => {
|
|
75
|
+
let query = getDb()(model);
|
|
76
|
+
query = applyWhere(query, where);
|
|
77
|
+
await query.del();
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
deleteMany: async ({ model, where }) => {
|
|
81
|
+
let query = getDb()(model);
|
|
82
|
+
query = applyWhere(query, where);
|
|
83
|
+
const count = await query.del();
|
|
84
|
+
return count;
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
count: async ({ model, where }) => {
|
|
88
|
+
let query = getDb()(model);
|
|
89
|
+
if (where) {
|
|
90
|
+
query = applyWhere(query, where);
|
|
91
|
+
}
|
|
92
|
+
const [{ count }] = await query.count("* as count");
|
|
93
|
+
return Number(count);
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const adapterConfig = {
|
|
99
|
+
adapterId: "sonamu-knex",
|
|
100
|
+
adapterName: "Sonamu Knex Adapter",
|
|
101
|
+
usePlural: false,
|
|
102
|
+
supportsJSON: true,
|
|
103
|
+
supportsDates: true,
|
|
104
|
+
supportsBooleans: true,
|
|
105
|
+
supportsNumericIds: false,
|
|
106
|
+
transaction: async <R>(cb: (trx: DBTransactionAdapter) => Promise<R>): Promise<R> => {
|
|
107
|
+
const db = DB.getDB("w");
|
|
108
|
+
return db.transaction(async (trx) => {
|
|
109
|
+
const options = lazyOptions;
|
|
110
|
+
if (!options) {
|
|
111
|
+
throw new Error("sonamuKnexAdapter: options not initialized");
|
|
112
|
+
}
|
|
113
|
+
return cb(
|
|
114
|
+
createAdapterFactory({
|
|
115
|
+
config: adapterConfig,
|
|
116
|
+
adapter: createCustomAdapter(() => trx),
|
|
117
|
+
})(options),
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const adapterCreator = createAdapterFactory({
|
|
124
|
+
config: adapterConfig,
|
|
125
|
+
adapter: createCustomAdapter(() => DB.getDB("w")),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return (options: BetterAuthOptions) => {
|
|
129
|
+
lazyOptions = options;
|
|
130
|
+
return adapterCreator(options);
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Better Auth의 공식 어댑터(Kysely, Drizzle, Prisma, MongoDB) 패턴에 맞춰
|
|
136
|
+
* AND 그룹과 OR 그룹을 분리한 뒤 top-level AND로 결합합니다.
|
|
137
|
+
* 결과: (A AND B AND ...) AND (C OR D OR ...)
|
|
138
|
+
*/
|
|
139
|
+
export function applyWhere(
|
|
140
|
+
query: Knex.QueryBuilder,
|
|
141
|
+
conditions: CleanedWhere[],
|
|
142
|
+
): Knex.QueryBuilder {
|
|
143
|
+
const andGroup = conditions.filter((c) => c.connector !== "OR");
|
|
144
|
+
const orGroup = conditions.filter((c) => c.connector === "OR");
|
|
145
|
+
|
|
146
|
+
if (andGroup.length > 0) {
|
|
147
|
+
for (const condition of andGroup) {
|
|
148
|
+
query = applyCondition(query, condition, "where");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (orGroup.length > 0) {
|
|
153
|
+
query = query.where(function (this: Knex.QueryBuilder) {
|
|
154
|
+
for (let i = 0; i < orGroup.length; i++) {
|
|
155
|
+
applyCondition(this, orGroup[i], i === 0 ? "where" : "orWhere");
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return query;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function applyCondition(
|
|
164
|
+
query: Knex.QueryBuilder,
|
|
165
|
+
condition: CleanedWhere,
|
|
166
|
+
method: "where" | "orWhere",
|
|
167
|
+
): Knex.QueryBuilder {
|
|
168
|
+
const { field, value, operator } = condition;
|
|
169
|
+
|
|
170
|
+
switch (operator) {
|
|
171
|
+
case "eq":
|
|
172
|
+
if (value === null) {
|
|
173
|
+
return query[method === "orWhere" ? "orWhereNull" : "whereNull"](field);
|
|
174
|
+
}
|
|
175
|
+
return query[method](field, "=", value);
|
|
176
|
+
case "ne":
|
|
177
|
+
if (value === null) {
|
|
178
|
+
return query[method === "orWhere" ? "orWhereNotNull" : "whereNotNull"](field);
|
|
179
|
+
}
|
|
180
|
+
return query[method](field, "!=", value);
|
|
181
|
+
case "lt":
|
|
182
|
+
return query[method](field, "<", value);
|
|
183
|
+
case "lte":
|
|
184
|
+
return query[method](field, "<=", value);
|
|
185
|
+
case "gt":
|
|
186
|
+
return query[method](field, ">", value);
|
|
187
|
+
case "gte":
|
|
188
|
+
return query[method](field, ">=", value);
|
|
189
|
+
case "in":
|
|
190
|
+
return query[method === "orWhere" ? "orWhereIn" : "whereIn"](
|
|
191
|
+
field,
|
|
192
|
+
value as (string | number)[],
|
|
193
|
+
);
|
|
194
|
+
case "not_in":
|
|
195
|
+
return query[method === "orWhere" ? "orWhereNotIn" : "whereNotIn"](
|
|
196
|
+
field,
|
|
197
|
+
value as (string | number)[],
|
|
198
|
+
);
|
|
199
|
+
case "contains":
|
|
200
|
+
return query[method](field, "like", `%${value}%`);
|
|
201
|
+
case "starts_with":
|
|
202
|
+
return query[method](field, "like", `${value}%`);
|
|
203
|
+
case "ends_with":
|
|
204
|
+
return query[method](field, "like", `%${value}`);
|
|
205
|
+
default:
|
|
206
|
+
return query;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const tsLoaderRegisterStateKey = Symbol.for("sonamu.ts-loader-register.state");
|
|
4
|
+
|
|
5
|
+
type TsLoaderRegisterState = {
|
|
6
|
+
registered: boolean;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type GlobalWithTsLoaderRegisterState = typeof globalThis & {
|
|
10
|
+
[tsLoaderRegisterStateKey]?: TsLoaderRegisterState;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function resetRegisterState() {
|
|
14
|
+
const globalState = globalThis as GlobalWithTsLoaderRegisterState;
|
|
15
|
+
delete globalState[tsLoaderRegisterStateKey];
|
|
16
|
+
delete process.env.SWCRC_PATH;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("ensureTsLoaderRegistered", () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
vi.resetModules();
|
|
22
|
+
vi.restoreAllMocks();
|
|
23
|
+
vi.unmock("node:module");
|
|
24
|
+
vi.unmock("../../utils/fs-utils.js");
|
|
25
|
+
resetRegisterState();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
vi.resetModules();
|
|
30
|
+
vi.restoreAllMocks();
|
|
31
|
+
vi.unmock("node:module");
|
|
32
|
+
vi.unmock("../../utils/fs-utils.js");
|
|
33
|
+
resetRegisterState();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("프로젝트 .swcrc를 우선 사용하고 중복 등록하지 않는다", async () => {
|
|
37
|
+
const registerMock = vi.fn();
|
|
38
|
+
vi.doMock("node:module", () => ({
|
|
39
|
+
register: registerMock,
|
|
40
|
+
}));
|
|
41
|
+
vi.doMock("../../utils/fs-utils.js", () => ({
|
|
42
|
+
exists: vi.fn(async (candidate: string) => candidate === "/tmp/fixture-api/.swcrc"),
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
const module = await import("../ts-loader-registration");
|
|
46
|
+
|
|
47
|
+
expect(registerMock).not.toHaveBeenCalled();
|
|
48
|
+
|
|
49
|
+
await module.ensureTsLoaderRegistered("/tmp/fixture-api");
|
|
50
|
+
|
|
51
|
+
expect(registerMock).toHaveBeenCalledTimes(1);
|
|
52
|
+
expect(registerMock).toHaveBeenCalledWith("@sonamu-kit/ts-loader/loader", {
|
|
53
|
+
parentURL: expect.stringContaining("/src/bin/ts-loader-registration"),
|
|
54
|
+
});
|
|
55
|
+
expect(process.env.SWCRC_PATH).toBe("/tmp/fixture-api/.swcrc");
|
|
56
|
+
|
|
57
|
+
await module.ensureTsLoaderRegistered("/tmp/another-api");
|
|
58
|
+
|
|
59
|
+
expect(registerMock).toHaveBeenCalledTimes(1);
|
|
60
|
+
expect(process.env.SWCRC_PATH).toBe("/tmp/fixture-api/.swcrc");
|
|
61
|
+
});
|
|
62
|
+
});
|