sonamu 0.10.8 → 0.10.10
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/decorators.d.ts.map +1 -1
- package/dist/api/decorators.js +5 -12
- package/dist/database/base-model.d.ts +1 -0
- package/dist/database/base-model.d.ts.map +1 -1
- package/dist/database/base-model.js +14 -8
- package/dist/database/db.d.ts +2 -1
- package/dist/database/db.d.ts.map +1 -1
- package/dist/database/db.js +13 -3
- package/dist/database/puri-wrapper.d.ts.map +1 -1
- package/dist/database/puri-wrapper.js +4 -11
- package/dist/database/transaction-context.d.ts +8 -3
- package/dist/database/transaction-context.d.ts.map +1 -1
- package/dist/database/transaction-context.js +12 -7
- package/dist/naite/naite-reporter.d.ts +8 -3
- package/dist/naite/naite-reporter.d.ts.map +1 -1
- package/dist/naite/naite-reporter.js +9 -4
- package/dist/template/implementations/services.template.js +3 -3
- package/package.json +2 -2
- package/src/api/decorators.ts +11 -22
- package/src/database/__tests__/transaction-scope.test.ts +227 -0
- package/src/database/base-model.ts +31 -6
- package/src/database/db.ts +17 -2
- package/src/database/puri-wrapper.ts +5 -22
- package/src/database/transaction-context.ts +14 -6
- package/src/naite/naite-reporter.ts +8 -3
- package/src/template/__tests__/services.template.mutation.test.ts +169 -0
- package/src/template/implementations/services.template.ts +3 -3
|
@@ -99,11 +99,8 @@ export class PuriWrapper<TSchema extends DatabaseSchemaExtend = DatabaseSchemaEx
|
|
|
99
99
|
): Promise<T> {
|
|
100
100
|
const { isolation, readOnly, dbPreset = "w" } = options;
|
|
101
101
|
|
|
102
|
-
// @transactional 데코레이터와 동일한 로직: 이미 트랜잭션 컨텍스트가 있는지 확인
|
|
103
102
|
const { DB } = await import("./db");
|
|
104
|
-
const existingContext = DB.transactionStorage.getStore();
|
|
105
103
|
|
|
106
|
-
// AsyncLocalStorage 컨텍스트가 없거나 해당 preset의 트랜잭션이 없으면 새로 시작
|
|
107
104
|
const startTransaction = async (
|
|
108
105
|
knex: Knex | Knex.Transaction,
|
|
109
106
|
upsertBuilder: UpsertBuilder,
|
|
@@ -112,33 +109,19 @@ export class PuriWrapper<TSchema extends DatabaseSchemaExtend = DatabaseSchemaEx
|
|
|
112
109
|
async (trx) => {
|
|
113
110
|
const trxWrapper = new PuriTransactionWrapper(trx, upsertBuilder);
|
|
114
111
|
|
|
115
|
-
|
|
116
|
-
DB.getTransactionContext().setTransaction(dbPreset, trxWrapper);
|
|
117
|
-
|
|
118
|
-
try {
|
|
119
|
-
return await callback(trxWrapper);
|
|
120
|
-
} finally {
|
|
121
|
-
// 트랜잭션 제거
|
|
122
|
-
DB.getTransactionContext().deleteTransaction(dbPreset);
|
|
123
|
-
}
|
|
112
|
+
return DB.runWithTransactionScope(dbPreset, trxWrapper, () => callback(trxWrapper));
|
|
124
113
|
},
|
|
125
114
|
{ isolationLevel: isolation, readOnly },
|
|
126
115
|
);
|
|
127
116
|
};
|
|
128
117
|
|
|
129
|
-
|
|
130
|
-
if (!existingContext) {
|
|
131
|
-
return DB.runWithTransaction(() => startTransaction(this.knex, this.upsertBuilder));
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// 해당 preset의 트랜잭션이 이미 있으면 SAVEPOINT로 중첩 트랜잭션 생성
|
|
135
|
-
const existingTrx = existingContext.getTransaction(dbPreset);
|
|
118
|
+
const existingTrx = DB.transactionStorage.getStore()?.getTransaction(dbPreset);
|
|
136
119
|
if (existingTrx) {
|
|
120
|
+
// 같은 preset의 중첩 트랜잭션은 기존 연결에서 SAVEPOINT로 시작한다.
|
|
137
121
|
return startTransaction(existingTrx.trx, existingTrx.upsertBuilder);
|
|
138
|
-
} else {
|
|
139
|
-
// 컨텍스트는 있지만 이 preset의 트랜잭션은 없는 경우 (같은 컨텍스트 내에서 실행)
|
|
140
|
-
return startTransaction(this.knex, this.upsertBuilder);
|
|
141
122
|
}
|
|
123
|
+
|
|
124
|
+
return startTransaction(this.knex, this.upsertBuilder);
|
|
142
125
|
}
|
|
143
126
|
|
|
144
127
|
ubRegister<TTable extends TableName<TSchema>>(
|
|
@@ -2,17 +2,25 @@ import { type DBPreset } from "./db";
|
|
|
2
2
|
import { type PuriTransactionWrapper } from "./puri-wrapper";
|
|
3
3
|
|
|
4
4
|
export class TransactionContext {
|
|
5
|
-
|
|
5
|
+
constructor(
|
|
6
|
+
private readonly parent?: TransactionContext,
|
|
7
|
+
private local?: {
|
|
8
|
+
preset: DBPreset;
|
|
9
|
+
transaction: PuriTransactionWrapper;
|
|
10
|
+
},
|
|
11
|
+
) {}
|
|
6
12
|
|
|
7
13
|
getTransaction(preset: DBPreset): PuriTransactionWrapper | undefined {
|
|
8
|
-
return this.
|
|
14
|
+
return this.local?.preset === preset
|
|
15
|
+
? this.local.transaction
|
|
16
|
+
: this.parent?.getTransaction(preset);
|
|
9
17
|
}
|
|
10
18
|
|
|
11
|
-
|
|
12
|
-
this.
|
|
19
|
+
getActiveTransaction(): PuriTransactionWrapper | undefined {
|
|
20
|
+
return this.local?.transaction ?? this.parent?.getActiveTransaction();
|
|
13
21
|
}
|
|
14
22
|
|
|
15
|
-
|
|
16
|
-
this.
|
|
23
|
+
clearLocal(): void {
|
|
24
|
+
this.local = undefined;
|
|
17
25
|
}
|
|
18
26
|
}
|
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
*
|
|
7
7
|
* 프로젝트별로 고유한 소켓을 사용하기 위해 sonamu.config.ts 경로의 해시를 사용합니다.
|
|
8
8
|
*
|
|
9
|
+
* 모듈 싱글턴이지만 인스턴스는 프로세스마다 별개입니다. run/start·run/end는
|
|
10
|
+
* Vitest 메인 프로세스(NaiteVitestReporter 커스텀 리포터)에서, test/result는
|
|
11
|
+
* 각 worker 프로세스(bootstrap의 afterEach)에서 전송되므로 buffer와 소켓
|
|
12
|
+
* 연결을 서로 공유하지 않습니다.
|
|
13
|
+
*
|
|
9
14
|
* fs mock 충돌을 피하기 위해 net 모듈만 사용합니다.
|
|
10
15
|
*/
|
|
11
16
|
/* oxlint-disable @typescript-eslint/no-explicit-any */ // Naite는 expect와 호응하도록 any를 허용함
|
|
@@ -112,7 +117,7 @@ class NaiteReporterClass {
|
|
|
112
117
|
}
|
|
113
118
|
|
|
114
119
|
/**
|
|
115
|
-
*
|
|
120
|
+
* Vitest 커스텀 리포터(NaiteVitestReporter)의 onTestRunStart에서 호출합니다.
|
|
116
121
|
* 테스트 run 시작을 알립니다 (데이터 클리어 신호).
|
|
117
122
|
*/
|
|
118
123
|
async startTestRun(): Promise<void> {
|
|
@@ -127,7 +132,7 @@ class NaiteReporterClass {
|
|
|
127
132
|
}
|
|
128
133
|
|
|
129
134
|
/**
|
|
130
|
-
* afterEach에서 호출합니다.
|
|
135
|
+
* bootstrap()의 afterEach에서 호출합니다.
|
|
131
136
|
* 테스트 케이스 결과를 traces와 함께 전송합니다.
|
|
132
137
|
*/
|
|
133
138
|
async reportTestResult(
|
|
@@ -145,7 +150,7 @@ class NaiteReporterClass {
|
|
|
145
150
|
}
|
|
146
151
|
|
|
147
152
|
/**
|
|
148
|
-
*
|
|
153
|
+
* Vitest 커스텀 리포터(NaiteVitestReporter)의 onTestRunEnd에서 호출합니다.
|
|
149
154
|
* 테스트 run 종료를 알립니다.
|
|
150
155
|
*/
|
|
151
156
|
async endTestRun(): Promise<void> {
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { Sonamu } from "../../api";
|
|
4
|
+
import { type ExtendedApi } from "../../api/decorators";
|
|
5
|
+
import { Template__services } from "../implementations/services.template";
|
|
6
|
+
|
|
7
|
+
describe("Template__services mutation hooks", () => {
|
|
8
|
+
let originalConfig: unknown;
|
|
9
|
+
let originalSyncer: unknown;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
originalConfig = Reflect.get(Sonamu, "_config");
|
|
13
|
+
originalSyncer = Reflect.get(Sonamu, "_syncer");
|
|
14
|
+
Reflect.set(Sonamu, "_config", {
|
|
15
|
+
api: {
|
|
16
|
+
route: {
|
|
17
|
+
prefix: "/api",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
Reflect.set(Sonamu, "_config", originalConfig);
|
|
25
|
+
Reflect.set(Sonamu, "_syncer", originalSyncer);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const renderServices = (apis: ExtendedApi[]) => {
|
|
29
|
+
Reflect.set(Sonamu, "_syncer", { apis });
|
|
30
|
+
return new Template__services().render({}).body;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
it("파라미터가 있는 일반 mutation에서 namespace와 인자 순서를 유지한다", () => {
|
|
34
|
+
const body = renderServices([
|
|
35
|
+
{
|
|
36
|
+
modelName: "CaseModel",
|
|
37
|
+
methodName: "leave",
|
|
38
|
+
path: "/case/leave",
|
|
39
|
+
options: {
|
|
40
|
+
httpMethod: "POST",
|
|
41
|
+
clients: ["tanstack-mutation"],
|
|
42
|
+
},
|
|
43
|
+
typeParameters: [],
|
|
44
|
+
parameters: [
|
|
45
|
+
{ name: "caseId", type: "number", optional: false },
|
|
46
|
+
{ name: "notify", type: "boolean", optional: false },
|
|
47
|
+
],
|
|
48
|
+
returnType: {
|
|
49
|
+
t: "ref",
|
|
50
|
+
id: "Promise",
|
|
51
|
+
args: ["boolean"],
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
expect(body).toContain(`export const useLeaveMutation = () => useMutation({
|
|
57
|
+
mutationFn: (params: { caseId: number, notify: boolean }) => CaseService.leave(params.caseId, params.notify)
|
|
58
|
+
});`);
|
|
59
|
+
expect(body).not.toContain(
|
|
60
|
+
"mutationFn: (params: { caseId: number, notify: boolean }) => leave(params.caseId, params.notify)",
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("파라미터가 없는 일반 mutation에서 params: void 타입을 유지한다", () => {
|
|
65
|
+
const body = renderServices([
|
|
66
|
+
{
|
|
67
|
+
modelName: "CaseModel",
|
|
68
|
+
methodName: "refresh",
|
|
69
|
+
path: "/case/refresh",
|
|
70
|
+
options: {
|
|
71
|
+
httpMethod: "POST",
|
|
72
|
+
clients: ["tanstack-mutation"],
|
|
73
|
+
},
|
|
74
|
+
typeParameters: [],
|
|
75
|
+
parameters: [],
|
|
76
|
+
returnType: {
|
|
77
|
+
t: "ref",
|
|
78
|
+
id: "Promise",
|
|
79
|
+
args: ["void"],
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
expect(body).toContain(`export const useRefreshMutation = () => useMutation({
|
|
85
|
+
mutationFn: (params: void) => CaseService.refresh()
|
|
86
|
+
});`);
|
|
87
|
+
expect(body).not.toContain("mutationFn: (params: void) => refresh()");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("파일만 받는 multipart mutation에서 namespace와 mutation 옵션을 유지한다", () => {
|
|
91
|
+
const body = renderServices([
|
|
92
|
+
{
|
|
93
|
+
modelName: "VoiceFrame",
|
|
94
|
+
methodName: "uploadRecording",
|
|
95
|
+
path: "/voice/uploadRecording",
|
|
96
|
+
options: {
|
|
97
|
+
httpMethod: "POST",
|
|
98
|
+
clients: ["axios-multipart", "tanstack-mutation-multipart"],
|
|
99
|
+
},
|
|
100
|
+
typeParameters: [],
|
|
101
|
+
parameters: [],
|
|
102
|
+
returnType: {
|
|
103
|
+
t: "ref",
|
|
104
|
+
id: "Promise",
|
|
105
|
+
args: ["string"],
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
expect(body).toContain(`export const useUploadRecordingMutation = (
|
|
111
|
+
options?: UseMutationOptions<string, Error, { files: File[] }> & {
|
|
112
|
+
onUploadProgress?: (e: AxiosProgressEvent) => void;
|
|
113
|
+
}
|
|
114
|
+
) => useMutation({
|
|
115
|
+
mutationFn: (params: { files: File[] }) => VoiceService.uploadRecording(params.files),
|
|
116
|
+
retry: false,
|
|
117
|
+
...options,
|
|
118
|
+
});`);
|
|
119
|
+
expect(body).not.toContain(
|
|
120
|
+
"mutationFn: (params: { files: File[] }) => uploadRecording(params.files)",
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("API 파라미터와 파일을 받는 multipart mutation에서 namespace와 인자 순서를 유지한다", () => {
|
|
125
|
+
const body = renderServices([
|
|
126
|
+
{
|
|
127
|
+
modelName: "VoiceFrame",
|
|
128
|
+
methodName: "transcript",
|
|
129
|
+
path: "/voice/transcript",
|
|
130
|
+
options: {
|
|
131
|
+
httpMethod: "POST",
|
|
132
|
+
clients: ["axios-multipart", "tanstack-mutation-multipart"],
|
|
133
|
+
},
|
|
134
|
+
typeParameters: [],
|
|
135
|
+
parameters: [
|
|
136
|
+
{
|
|
137
|
+
name: "params",
|
|
138
|
+
type: {
|
|
139
|
+
t: "object",
|
|
140
|
+
props: [
|
|
141
|
+
{ name: "language", type: "string", optional: false },
|
|
142
|
+
{ name: "speakerCount", type: "number", optional: false },
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
optional: false,
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
returnType: {
|
|
149
|
+
t: "ref",
|
|
150
|
+
id: "Promise",
|
|
151
|
+
args: ["number"],
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
]);
|
|
155
|
+
|
|
156
|
+
expect(body).toContain(`export const useTranscriptMutation = (
|
|
157
|
+
options?: UseMutationOptions<number, Error, { params: { language: string, speakerCount: number }, files: File[] }> & {
|
|
158
|
+
onUploadProgress?: (e: AxiosProgressEvent) => void;
|
|
159
|
+
}
|
|
160
|
+
) => useMutation({
|
|
161
|
+
mutationFn: (params: { params: { language: string, speakerCount: number }, files: File[] }) => VoiceService.transcript(params.params, params.files),
|
|
162
|
+
retry: false,
|
|
163
|
+
...options,
|
|
164
|
+
});`);
|
|
165
|
+
expect(body).not.toContain(
|
|
166
|
+
"mutationFn: (params: { params: { language: string, speakerCount: number }, files: File[] }) => transcript(params.params, params.files)",
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -353,7 +353,7 @@ export const ${infiniteHookName} = ${typeParamsDef}(${paramsDef}${
|
|
|
353
353
|
functions.push(
|
|
354
354
|
`
|
|
355
355
|
export const use${hookName}Mutation = ${typeParamsDef}() => useMutation({
|
|
356
|
-
mutationFn: (params: ${mutationParamType}) => ${methodName}(${mutationParamNames})
|
|
356
|
+
mutationFn: (params: ${mutationParamType}) => ${modelName}Service.${methodName}(${mutationParamNames})
|
|
357
357
|
});
|
|
358
358
|
`.trim(),
|
|
359
359
|
);
|
|
@@ -378,8 +378,8 @@ export const use${hookName}Mutation = ${typeParamsDef}() => useMutation({
|
|
|
378
378
|
// mutationFn 호출 파라미터
|
|
379
379
|
const mutationFnCall =
|
|
380
380
|
paramsWithoutContext.length > 0
|
|
381
|
-
? `${methodName}(params.params, params.files)`
|
|
382
|
-
: `${methodName}(params.files)`;
|
|
381
|
+
? `${modelName}Service.${methodName}(params.params, params.files)`
|
|
382
|
+
: `${modelName}Service.${methodName}(params.files)`;
|
|
383
383
|
|
|
384
384
|
functions.push(
|
|
385
385
|
`
|