@axyl-tcb/create-template 1.0.15 → 1.0.17
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/package.json +1 -1
- package/template/MYSQL.md +72 -2
- package/template/README.md +14 -2
package/package.json
CHANGED
package/template/MYSQL.md
CHANGED
|
@@ -12,6 +12,7 @@ TCB Cloud Function 에서 `mysql2` 를 통해 MySQL 을 사용하는 방법을
|
|
|
12
12
|
- [트랜잭션](#트랜잭션)
|
|
13
13
|
- [에러 처리](#에러-처리)
|
|
14
14
|
- [baseHandler 통합 예시](#basehandler-통합-예시)
|
|
15
|
+
- [테스트](#테스트)
|
|
15
16
|
- [활용편 — 멱등성 키로 중복 요청 차단하기](#활용편--멱등성-키로-중복-요청-차단하기)
|
|
16
17
|
|
|
17
18
|
---
|
|
@@ -115,6 +116,8 @@ const [rows] = await pool.query<RowDataPacket[]>(
|
|
|
115
116
|
);
|
|
116
117
|
```
|
|
117
118
|
|
|
119
|
+
> `LIMIT` · `OFFSET` 에도 `?` 플레이스홀더를 쓸 수 있습니다. 단 이 템플릿처럼 `pool.query` 를 사용하세요 — `pool.execute`(prepared statement)는 일부 mysql2 버전에서 LIMIT 바인딩을 거부합니다.
|
|
120
|
+
|
|
118
121
|
### IN 조건
|
|
119
122
|
|
|
120
123
|
```ts
|
|
@@ -280,7 +283,7 @@ try {
|
|
|
280
283
|
} catch (err: unknown) {
|
|
281
284
|
const mysqlErr = err as { code?: string };
|
|
282
285
|
if (mysqlErr.code === 'ER_DUP_ENTRY') {
|
|
283
|
-
throw new AxylMiddlewareError(ERROR_CODES.
|
|
286
|
+
throw new AxylMiddlewareError(ERROR_CODES.INVALID_PARAMETER, '이미 존재하는 항목입니다.');
|
|
284
287
|
}
|
|
285
288
|
throw err; // 나머지는 INTERNAL_ERROR 로 처리
|
|
286
289
|
}
|
|
@@ -351,6 +354,73 @@ export const main = createAxylHandler(baseHandler, {
|
|
|
351
354
|
|
|
352
355
|
---
|
|
353
356
|
|
|
357
|
+
## 테스트
|
|
358
|
+
|
|
359
|
+
핸들러는 실제 DB 연결 없이 테스트합니다. `@axyl-tcb/middleware/db` 를 `vi.mock` 으로 대체하면 됩니다. mock 객체는 `vi.hoisted` 로 먼저 만들어야 hoisting 문제가 없습니다.
|
|
360
|
+
|
|
361
|
+
> mock 반환 형태는 mysql2 와 동일한 `[rows, fields]` 튜플입니다. SELECT 는 `[[...rows], []]`, INSERT/UPDATE 는 `[{ insertId, affectedRows }, []]` 처럼 첫 요소만 맞추면 됩니다.
|
|
362
|
+
|
|
363
|
+
### pool.query 기반 (SELECT / INSERT / UPDATE / DELETE)
|
|
364
|
+
|
|
365
|
+
```ts
|
|
366
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
367
|
+
|
|
368
|
+
const { mockPool } = vi.hoisted(() => ({
|
|
369
|
+
mockPool: { query: vi.fn() },
|
|
370
|
+
}));
|
|
371
|
+
|
|
372
|
+
vi.mock('@axyl-tcb/middleware/db', () => ({ pool: mockPool }));
|
|
373
|
+
|
|
374
|
+
import { main } from '../../src/functions/mysql/getInventory';
|
|
375
|
+
|
|
376
|
+
describe('getInventory handler', () => {
|
|
377
|
+
const event = { headers: { 'X-Hive-Player-Id': 'p1', 'X-Hive-Aud': '10-1001-5' } };
|
|
378
|
+
beforeEach(() => vi.clearAllMocks());
|
|
379
|
+
|
|
380
|
+
it('보상 목록을 조회한다', async () => {
|
|
381
|
+
mockPool.query.mockResolvedValueOnce([[{ id: 1, item_id: 10, quantity: 3 }]]);
|
|
382
|
+
|
|
383
|
+
const result = await main(event, {});
|
|
384
|
+
|
|
385
|
+
expect(result.success).toBe(true);
|
|
386
|
+
if (!result.success) return;
|
|
387
|
+
expect(result.data.rewards).toHaveLength(1);
|
|
388
|
+
|
|
389
|
+
const [sql, params] = mockPool.query.mock.calls[0]!;
|
|
390
|
+
expect(sql).toContain('FROM rewards');
|
|
391
|
+
expect(params).toEqual(['p1', 1001, 20]); // playerId, aud.gameIndex, limit
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### transaction 기반
|
|
397
|
+
|
|
398
|
+
`transaction` 헬퍼를 mock 해 콜백에 가짜 커넥션(`conn`)을 넘깁니다. 단계별 쿼리 결과는 `mockConn.query.mockResolvedValueOnce(...)` 로 순서대로 지정합니다.
|
|
399
|
+
|
|
400
|
+
```ts
|
|
401
|
+
const { mockConn, mockTransaction } = vi.hoisted(() => ({
|
|
402
|
+
mockConn: { query: vi.fn() },
|
|
403
|
+
mockTransaction: vi.fn(),
|
|
404
|
+
}));
|
|
405
|
+
|
|
406
|
+
vi.mock('@axyl-tcb/middleware/db', () => ({
|
|
407
|
+
pool: { query: vi.fn() },
|
|
408
|
+
transaction: mockTransaction,
|
|
409
|
+
}));
|
|
410
|
+
|
|
411
|
+
beforeEach(() => {
|
|
412
|
+
vi.clearAllMocks();
|
|
413
|
+
// transaction(fn) 이 fn(conn) 을 그대로 실행하도록 연결
|
|
414
|
+
mockTransaction.mockImplementation(
|
|
415
|
+
(fn: (conn: typeof mockConn) => Promise<unknown>) => fn(mockConn)
|
|
416
|
+
);
|
|
417
|
+
});
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
실제 예시는 생성된 프로젝트의 `test/mysql/grantItem.test.ts`(transaction) 와 `test/mysql/drawGacha.test.ts`(pool + transaction) 를 참고하세요.
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
354
424
|
## 활용편 — 멱등성 키로 중복 요청 차단하기
|
|
355
425
|
|
|
356
426
|
### 왜 필요한가
|
|
@@ -488,7 +558,7 @@ const baseHandler: AxylBaseHandler<RewardReqDto, RewardResDto> = async (
|
|
|
488
558
|
|
|
489
559
|
### 실제 예제 — drawGacha (확률성 작업 = 결과 캐싱 필수)
|
|
490
560
|
|
|
491
|
-
`src/functions/drawGacha.ts` 는 패턴 1(결과 캐싱)을 가챠 뽑기에 적용한 예제입니다.
|
|
561
|
+
`src/functions/mysql/drawGacha.ts` 는 패턴 1(결과 캐싱)을 가챠 뽑기에 적용한 예제입니다.
|
|
492
562
|
|
|
493
563
|
가챠처럼 **결과가 확률로 정해지는 작업**은 재시도 시 재실행하면 결과가 달라지므로, 단순 중복 차단(패턴 2)으론 부족하고 **원래 뽑힌 결과를 저장했다가 그대로 재반환**해야 합니다.
|
|
494
564
|
|
package/template/README.md
CHANGED
|
@@ -361,12 +361,24 @@ export const main = createAxylHandler(baseHandler);
|
|
|
361
361
|
"TCB_DOCDB_INSTANCE": "",
|
|
362
362
|
"TCB_DOCDB_DATABASE": ""
|
|
363
363
|
}
|
|
364
|
+
|
|
365
|
+
// Cloud Storage + MySQL 을 함께 쓰는 경우 (예: uploadFile, deleteFile — 파일 저장 + files 테이블 기록)
|
|
366
|
+
"envVariables": {
|
|
367
|
+
"TCB_ENV_ID": "",
|
|
368
|
+
"DB_HOST": "",
|
|
369
|
+
"DB_PORT": "",
|
|
370
|
+
"DB_USER": "",
|
|
371
|
+
"DB_PASSWORD": "",
|
|
372
|
+
"DB_NAME": ""
|
|
373
|
+
}
|
|
364
374
|
```
|
|
365
375
|
|
|
366
376
|
### 3. 테스트 작성
|
|
367
377
|
|
|
368
|
-
`test/<테마>/myFunction.test.ts` 를 작성합니다.
|
|
369
|
-
|
|
378
|
+
`test/<테마>/myFunction.test.ts` 를 작성합니다. 실제 DB/TCB 연결 없이 미들웨어 서브패스를 `vi.mock` 으로 대체합니다.
|
|
379
|
+
|
|
380
|
+
- 단순 쿼리(`pool.query`) → `test/mysql/drawGacha.test.ts` 또는 [MYSQL.md 테스트](./MYSQL.md#테스트) 의 *pool.query 기반* 예시
|
|
381
|
+
- 트랜잭션(`transaction`) → `test/mysql/grantItem.test.ts` 또는 [MYSQL.md 테스트](./MYSQL.md#테스트) 의 *transaction 기반* 예시
|
|
370
382
|
|
|
371
383
|
---
|
|
372
384
|
|