refactor: apply new utilities and constants across codebase
P0 fixes: - KV Cache migration: security.ts now delegates to kv-cache.ts (74% code reduction) - Environment validation: index.ts validates env on first request - Type safety: optimistic-lock.ts removes `as any` with proper interface P1 improvements: - Constants applied to deposit-agent.ts (TRANSACTION_STATUS, TRANSACTION_TYPE) - Constants applied to callback-handler.ts (CALLBACK_PREFIXES) - Constants applied to domain-tool.ts (MESSAGE_MARKERS) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -48,12 +48,17 @@ export const KEYBOARD_TYPES = {
|
|||||||
*
|
*
|
||||||
* Format: prefix:action:params
|
* Format: prefix:action:params
|
||||||
* Examples:
|
* Examples:
|
||||||
* - domain_reg:confirm:example.com:15000
|
* - domain_reg:example.com:15000
|
||||||
* - server_order:confirm:order_123
|
* - server_order:userId:index
|
||||||
|
* - server_cancel:userId
|
||||||
*/
|
*/
|
||||||
export const CALLBACK_PREFIXES = {
|
export const CALLBACK_PREFIXES = {
|
||||||
DOMAIN_REGISTER_CONFIRM: 'confirm_domain_register',
|
DOMAIN_REGISTER: 'domain_reg',
|
||||||
DOMAIN_REGISTER_CANCEL: 'cancel_domain_register',
|
DOMAIN_CANCEL: 'domain_cancel',
|
||||||
|
SERVER_ORDER: 'server_order',
|
||||||
|
SERVER_CANCEL: 'server_cancel',
|
||||||
|
CONFIRM_DOMAIN_REGISTER: 'confirm_domain_register',
|
||||||
|
CANCEL_DOMAIN_REGISTER: 'cancel_domain_register',
|
||||||
SERVER_ORDER_CONFIRM: 'confirm_server_order',
|
SERVER_ORDER_CONFIRM: 'confirm_server_order',
|
||||||
SERVER_ORDER_CANCEL: 'cancel_server_order',
|
SERVER_ORDER_CANCEL: 'cancel_server_order',
|
||||||
DELETE_CONFIRM: 'confirm_delete',
|
DELETE_CONFIRM: 'confirm_delete',
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
import { createLogger } from './utils/logger';
|
import { createLogger } from './utils/logger';
|
||||||
import { executeWithOptimisticLock, OptimisticLockError } from './utils/optimistic-lock';
|
import { executeWithOptimisticLock, OptimisticLockError } from './utils/optimistic-lock';
|
||||||
|
import { TRANSACTION_STATUS, TRANSACTION_TYPE } from './constants';
|
||||||
import type { ManageDepositArgs, DepositFunctionResult } from './types';
|
import type { ManageDepositArgs, DepositFunctionResult } from './types';
|
||||||
|
|
||||||
const logger = createLogger('deposit-agent');
|
const logger = createLogger('deposit-agent');
|
||||||
@@ -108,8 +109,8 @@ export async function executeDepositFunction(
|
|||||||
// 1. Insert transaction record
|
// 1. Insert transaction record
|
||||||
const result = await db.prepare(
|
const result = await db.prepare(
|
||||||
`INSERT INTO deposit_transactions (user_id, type, amount, status, depositor_name, depositor_name_prefix, description, confirmed_at)
|
`INSERT INTO deposit_transactions (user_id, type, amount, status, depositor_name, depositor_name_prefix, description, confirmed_at)
|
||||||
VALUES (?, 'deposit', ?, 'confirmed', ?, ?, '입금 확인', CURRENT_TIMESTAMP)`
|
VALUES (?, ?, ?, ?, ?, ?, '입금 확인', CURRENT_TIMESTAMP)`
|
||||||
).bind(userId, amount, depositor_name, depositor_name.slice(0, 7)).run();
|
).bind(userId, TRANSACTION_TYPE.DEPOSIT, amount, TRANSACTION_STATUS.CONFIRMED, depositor_name, depositor_name.slice(0, 7)).run();
|
||||||
|
|
||||||
const txId = result.meta.last_row_id;
|
const txId = result.meta.last_row_id;
|
||||||
|
|
||||||
@@ -178,8 +179,8 @@ export async function executeDepositFunction(
|
|||||||
// 은행 알림이 없으면 pending 거래 생성
|
// 은행 알림이 없으면 pending 거래 생성
|
||||||
const result = await db.prepare(
|
const result = await db.prepare(
|
||||||
`INSERT INTO deposit_transactions (user_id, type, amount, status, depositor_name, depositor_name_prefix, description)
|
`INSERT INTO deposit_transactions (user_id, type, amount, status, depositor_name, depositor_name_prefix, description)
|
||||||
VALUES (?, 'deposit', ?, 'pending', ?, ?, '입금 대기')`
|
VALUES (?, ?, ?, ?, ?, ?, '입금 대기')`
|
||||||
).bind(userId, amount, depositor_name, depositor_name.slice(0, 7)).run();
|
).bind(userId, TRANSACTION_TYPE.DEPOSIT, amount, TRANSACTION_STATUS.PENDING, depositor_name, depositor_name.slice(0, 7)).run();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -187,7 +188,7 @@ export async function executeDepositFunction(
|
|||||||
transaction_id: result.meta.last_row_id,
|
transaction_id: result.meta.last_row_id,
|
||||||
amount: amount,
|
amount: amount,
|
||||||
depositor_name: depositor_name,
|
depositor_name: depositor_name,
|
||||||
status: 'pending',
|
status: TRANSACTION_STATUS.PENDING,
|
||||||
message: '입금 요청이 등록되었습니다. 은행 입금 확인 후 자동으로 충전됩니다.',
|
message: '입금 요청이 등록되었습니다. 은행 입금 확인 후 자동으로 충전됩니다.',
|
||||||
account_info: {
|
account_info: {
|
||||||
bank: context.env?.DEPOSIT_BANK_NAME || '하나은행',
|
bank: context.env?.DEPOSIT_BANK_NAME || '하나은행',
|
||||||
@@ -253,13 +254,13 @@ export async function executeDepositFunction(
|
|||||||
if (tx.user_id !== userId && !isAdmin) {
|
if (tx.user_id !== userId && !isAdmin) {
|
||||||
return { error: '본인의 거래만 취소할 수 있습니다.' };
|
return { error: '본인의 거래만 취소할 수 있습니다.' };
|
||||||
}
|
}
|
||||||
if (tx.status !== 'pending') {
|
if (tx.status !== TRANSACTION_STATUS.PENDING) {
|
||||||
return { error: '대기 중인 거래만 취소할 수 있습니다.' };
|
return { error: '대기 중인 거래만 취소할 수 있습니다.' };
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.prepare(
|
await db.prepare(
|
||||||
"UPDATE deposit_transactions SET status = 'cancelled' WHERE id = ?"
|
"UPDATE deposit_transactions SET status = ? WHERE id = ?"
|
||||||
).bind(transaction_id).run();
|
).bind(TRANSACTION_STATUS.CANCELLED, transaction_id).run();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -278,9 +279,9 @@ export async function executeDepositFunction(
|
|||||||
`SELECT dt.id, dt.amount, dt.depositor_name, dt.created_at, u.telegram_id, u.username
|
`SELECT dt.id, dt.amount, dt.depositor_name, dt.created_at, u.telegram_id, u.username
|
||||||
FROM deposit_transactions dt
|
FROM deposit_transactions dt
|
||||||
JOIN users u ON dt.user_id = u.id
|
JOIN users u ON dt.user_id = u.id
|
||||||
WHERE dt.status = 'pending' AND dt.type = 'deposit'
|
WHERE dt.status = ? AND dt.type = ?
|
||||||
ORDER BY dt.created_at ASC`
|
ORDER BY dt.created_at ASC`
|
||||||
).all<{
|
).bind(TRANSACTION_STATUS.PENDING, TRANSACTION_TYPE.DEPOSIT).all<{
|
||||||
id: number;
|
id: number;
|
||||||
amount: number;
|
amount: number;
|
||||||
depositor_name: string;
|
depositor_name: string;
|
||||||
@@ -321,7 +322,7 @@ export async function executeDepositFunction(
|
|||||||
if (!tx) {
|
if (!tx) {
|
||||||
return { error: '거래를 찾을 수 없습니다.' };
|
return { error: '거래를 찾을 수 없습니다.' };
|
||||||
}
|
}
|
||||||
if (tx.status !== 'pending') {
|
if (tx.status !== TRANSACTION_STATUS.PENDING) {
|
||||||
return { error: '대기 중인 거래만 확인할 수 있습니다.' };
|
return { error: '대기 중인 거래만 확인할 수 있습니다.' };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,8 +331,8 @@ export async function executeDepositFunction(
|
|||||||
await executeWithOptimisticLock(db, async (attempt) => {
|
await executeWithOptimisticLock(db, async (attempt) => {
|
||||||
// 1. Update transaction status
|
// 1. Update transaction status
|
||||||
const txUpdate = await db.prepare(
|
const txUpdate = await db.prepare(
|
||||||
"UPDATE deposit_transactions SET status = 'confirmed', confirmed_at = CURRENT_TIMESTAMP WHERE id = ? AND status = 'pending'"
|
"UPDATE deposit_transactions SET status = ?, confirmed_at = CURRENT_TIMESTAMP WHERE id = ? AND status = ?"
|
||||||
).bind(transaction_id).run();
|
).bind(TRANSACTION_STATUS.CONFIRMED, transaction_id, TRANSACTION_STATUS.PENDING).run();
|
||||||
|
|
||||||
if (!txUpdate.success || txUpdate.meta.changes === 0) {
|
if (!txUpdate.success || txUpdate.meta.changes === 0) {
|
||||||
throw new Error('Transaction already processed or not found');
|
throw new Error('Transaction already processed or not found');
|
||||||
@@ -406,13 +407,13 @@ export async function executeDepositFunction(
|
|||||||
if (!tx) {
|
if (!tx) {
|
||||||
return { error: '거래를 찾을 수 없습니다.' };
|
return { error: '거래를 찾을 수 없습니다.' };
|
||||||
}
|
}
|
||||||
if (tx.status !== 'pending') {
|
if (tx.status !== TRANSACTION_STATUS.PENDING) {
|
||||||
return { error: '대기 중인 거래만 거절할 수 있습니다.' };
|
return { error: '대기 중인 거래만 거절할 수 있습니다.' };
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.prepare(
|
await db.prepare(
|
||||||
"UPDATE deposit_transactions SET status = 'rejected' WHERE id = ?"
|
"UPDATE deposit_transactions SET status = ? WHERE id = ?"
|
||||||
).bind(transaction_id).run();
|
).bind(TRANSACTION_STATUS.REJECTED, transaction_id).run();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
36
src/index.ts
36
src/index.ts
@@ -10,13 +10,49 @@ import { handleProvisionQueue, handleProvisionDLQ } from './server-provision';
|
|||||||
import { timingSafeEqual } from './security';
|
import { timingSafeEqual } from './security';
|
||||||
import { createLogger } from './utils/logger';
|
import { createLogger } from './utils/logger';
|
||||||
import { notifyAdmin } from './services/notification';
|
import { notifyAdmin } from './services/notification';
|
||||||
|
import { validateEnv } from './utils/env-validation';
|
||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
|
|
||||||
const logger = createLogger('worker');
|
const logger = createLogger('worker');
|
||||||
|
|
||||||
|
// Environment validation flag (checked once per instance)
|
||||||
|
let envValidated = false;
|
||||||
|
|
||||||
// Hono app with Env type
|
// Hono app with Env type
|
||||||
const app = new Hono<{ Bindings: Env }>();
|
const app = new Hono<{ Bindings: Env }>();
|
||||||
|
|
||||||
|
// Environment validation middleware (runs once per worker instance)
|
||||||
|
app.use('*', async (c, next) => {
|
||||||
|
if (!envValidated) {
|
||||||
|
// Cast to Record<string, unknown> for validation
|
||||||
|
const result = validateEnv(c.env as unknown as Record<string, unknown>);
|
||||||
|
if (!result.success) {
|
||||||
|
logger.error('Environment validation failed on startup', new Error('Invalid configuration'), {
|
||||||
|
errors: result.errors,
|
||||||
|
});
|
||||||
|
return c.json({
|
||||||
|
error: 'Configuration error',
|
||||||
|
message: 'The worker is not properly configured. Please check environment variables.',
|
||||||
|
details: result.errors,
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log warnings but continue
|
||||||
|
if (result.warnings.length > 0) {
|
||||||
|
logger.warn('Environment configuration warnings', { warnings: result.warnings });
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('Environment validation passed', {
|
||||||
|
environment: c.env.ENVIRONMENT || 'production',
|
||||||
|
warnings: result.warnings.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
envValidated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await next();
|
||||||
|
});
|
||||||
|
|
||||||
// Health check (public - minimal info only)
|
// Health check (public - minimal info only)
|
||||||
app.get('/health', () => handleHealthCheck());
|
app.get('/health', () => handleHealthCheck());
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { answerCallbackQuery, editMessageText, sendMessage } from '../../telegra
|
|||||||
import { UserService } from '../../services/user-service';
|
import { UserService } from '../../services/user-service';
|
||||||
import { executeDomainRegister } from '../../domain-register';
|
import { executeDomainRegister } from '../../domain-register';
|
||||||
import { createLogger } from '../../utils/logger';
|
import { createLogger } from '../../utils/logger';
|
||||||
|
import { CALLBACK_PREFIXES } from '../../constants';
|
||||||
import type { Env, TelegramUpdate } from '../../types';
|
import type { Env, TelegramUpdate } from '../../types';
|
||||||
|
|
||||||
const logger = createLogger('callback-handler');
|
const logger = createLogger('callback-handler');
|
||||||
@@ -43,7 +44,7 @@ export async function handleCallbackQuery(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 도메인 등록 처리
|
// 도메인 등록 처리
|
||||||
if (data.startsWith('domain_reg:')) {
|
if (data.startsWith(`${CALLBACK_PREFIXES.DOMAIN_REGISTER}:`)) {
|
||||||
const parts = data.split(':');
|
const parts = data.split(':');
|
||||||
if (parts.length !== 3) {
|
if (parts.length !== 3) {
|
||||||
await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '잘못된 데이터입니다.' });
|
await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '잘못된 데이터입니다.' });
|
||||||
@@ -145,7 +146,7 @@ ${result.error}
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 도메인 등록 취소
|
// 도메인 등록 취소
|
||||||
if (data === 'domain_cancel') {
|
if (data === CALLBACK_PREFIXES.DOMAIN_CANCEL) {
|
||||||
await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '취소되었습니다.' });
|
await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '취소되었습니다.' });
|
||||||
await editMessageText(
|
await editMessageText(
|
||||||
env.BOT_TOKEN,
|
env.BOT_TOKEN,
|
||||||
@@ -157,7 +158,7 @@ ${result.error}
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 서버 주문 확인
|
// 서버 주문 확인
|
||||||
if (data.startsWith('server_order:')) {
|
if (data.startsWith(`${CALLBACK_PREFIXES.SERVER_ORDER}:`)) {
|
||||||
const parts = data.split(':');
|
const parts = data.split(':');
|
||||||
if (parts.length !== 3) {
|
if (parts.length !== 3) {
|
||||||
await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '잘못된 데이터입니다.' });
|
await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '잘못된 데이터입니다.' });
|
||||||
@@ -325,7 +326,7 @@ ${result.error}
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 서버 주문 취소
|
// 서버 주문 취소
|
||||||
if (data.startsWith('server_cancel:')) {
|
if (data.startsWith(`${CALLBACK_PREFIXES.SERVER_CANCEL}:`)) {
|
||||||
const parts = data.split(':');
|
const parts = data.split(':');
|
||||||
if (parts.length !== 2) {
|
if (parts.length !== 2) {
|
||||||
await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '잘못된 데이터입니다.' });
|
await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '잘못된 데이터입니다.' });
|
||||||
|
|||||||
@@ -138,12 +138,8 @@ export async function validateWebhookRequest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Rate Limiting (Cloudflare KV 기반)
|
// Rate Limiting (Cloudflare KV 기반)
|
||||||
// NOTE: Future migration to KVCache abstraction layer (kv-cache.ts) planned
|
// Migrated to use KVCache abstraction layer (kv-cache.ts)
|
||||||
// Current implementation kept for backward compatibility
|
import { createRateLimitCache, checkRateLimitWithCache } from './services/kv-cache';
|
||||||
interface RateLimitData {
|
|
||||||
count: number;
|
|
||||||
resetAt: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function checkRateLimit(
|
export async function checkRateLimit(
|
||||||
kv: KVNamespace,
|
kv: KVNamespace,
|
||||||
@@ -151,61 +147,12 @@ export async function checkRateLimit(
|
|||||||
maxRequests: number = 30,
|
maxRequests: number = 30,
|
||||||
windowMs: number = 60000
|
windowMs: number = 60000
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const key = `ratelimit:${userId}`;
|
// Convert windowMs (milliseconds) to windowSeconds (seconds)
|
||||||
const now = Date.now();
|
const windowSeconds = Math.ceil(windowMs / 1000);
|
||||||
const logger = createLogger('rate-limit');
|
|
||||||
|
|
||||||
try {
|
// Create KVCache instance with 'rate:' prefix
|
||||||
// KV에서 기존 데이터 조회
|
const cache = createRateLimitCache(kv);
|
||||||
const dataStr = await kv.get(key);
|
|
||||||
const data: RateLimitData | null = dataStr ? JSON.parse(dataStr) : null;
|
|
||||||
|
|
||||||
// 윈도우 만료 또는 첫 요청
|
// Delegate to unified KV cache implementation
|
||||||
if (!data || now > data.resetAt) {
|
return checkRateLimitWithCache(cache, userId, maxRequests, windowSeconds);
|
||||||
const newData: RateLimitData = {
|
|
||||||
count: 1,
|
|
||||||
resetAt: now + windowMs,
|
|
||||||
};
|
|
||||||
await kv.put(key, JSON.stringify(newData), {
|
|
||||||
expirationTtl: Math.ceil(windowMs / 1000), // 초 단위
|
|
||||||
});
|
|
||||||
logger.info('Rate limit 윈도우 시작', {
|
|
||||||
userId,
|
|
||||||
resetAt: new Date(newData.resetAt).toISOString(),
|
|
||||||
maxRequests,
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rate limit 초과
|
|
||||||
if (data.count >= maxRequests) {
|
|
||||||
const resetInSeconds = Math.ceil((data.resetAt - now) / 1000);
|
|
||||||
logger.warn('Rate limit 초과', {
|
|
||||||
userId,
|
|
||||||
currentCount: data.count,
|
|
||||||
maxRequests,
|
|
||||||
resetInSeconds,
|
|
||||||
resetAt: new Date(data.resetAt).toISOString(),
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 카운트 증가
|
|
||||||
const updatedData: RateLimitData = {
|
|
||||||
count: data.count + 1,
|
|
||||||
resetAt: data.resetAt,
|
|
||||||
};
|
|
||||||
const remainingTtl = Math.ceil((data.resetAt - now) / 1000);
|
|
||||||
await kv.put(key, JSON.stringify(updatedData), {
|
|
||||||
expirationTtl: Math.max(remainingTtl, 1), // 최소 1초
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
// KV 오류 시 요청 허용 (fail-open)
|
|
||||||
// Rate limiting은 abuse 방지 목적이므로 가용성 우선
|
|
||||||
// 심각한 abuse는 Cloudflare WAF/Firewall Rules로 별도 대응
|
|
||||||
logger.warn('KV 오류 - 요청 허용 (fail-open)', { userId, error: (error as Error).message });
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { retryWithBackoff, RetryError } from '../utils/retry';
|
|||||||
import { createLogger, maskUserId } from '../utils/logger';
|
import { createLogger, maskUserId } from '../utils/logger';
|
||||||
import { getOpenAIUrl } from '../utils/api-urls';
|
import { getOpenAIUrl } from '../utils/api-urls';
|
||||||
import { ERROR_MESSAGES } from '../constants/messages';
|
import { ERROR_MESSAGES } from '../constants/messages';
|
||||||
|
import { MESSAGE_MARKERS } from '../constants';
|
||||||
|
|
||||||
const logger = createLogger('domain-tool');
|
const logger = createLogger('domain-tool');
|
||||||
|
|
||||||
@@ -796,7 +797,7 @@ async function executeDomainAction(
|
|||||||
domain: domain,
|
domain: domain,
|
||||||
price: price
|
price: price
|
||||||
});
|
});
|
||||||
return `__KEYBOARD__${keyboardData}__END__
|
return `${MESSAGE_MARKERS.KEYBOARD}${keyboardData}${MESSAGE_MARKERS.KEYBOARD_END}
|
||||||
📋 <b>도메인 등록 확인</b>
|
📋 <b>도메인 등록 확인</b>
|
||||||
|
|
||||||
• 도메인: <code>${domain}</code>
|
• 도메인: <code>${domain}</code>
|
||||||
|
|||||||
@@ -32,6 +32,13 @@ import { createLogger } from './logger';
|
|||||||
|
|
||||||
const logger = createLogger('optimistic-lock');
|
const logger = createLogger('optimistic-lock');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for Error with captureStackTrace method
|
||||||
|
*/
|
||||||
|
interface ErrorWithCapture {
|
||||||
|
captureStackTrace?: (target: object, constructor?: Function) => void;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom error for optimistic lock failures
|
* Custom error for optimistic lock failures
|
||||||
*/
|
*/
|
||||||
@@ -40,10 +47,9 @@ export class OptimisticLockError extends Error {
|
|||||||
super(message);
|
super(message);
|
||||||
this.name = 'OptimisticLockError';
|
this.name = 'OptimisticLockError';
|
||||||
// Maintain proper stack trace for debugging (Node.js only, not available in Workers)
|
// Maintain proper stack trace for debugging (Node.js only, not available in Workers)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
const ErrorConstructor = Error as unknown as ErrorWithCapture;
|
||||||
if (typeof (Error as any).captureStackTrace === 'function') {
|
if (typeof ErrorConstructor.captureStackTrace === 'function') {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
ErrorConstructor.captureStackTrace(this, OptimisticLockError);
|
||||||
(Error as any).captureStackTrace(this, OptimisticLockError);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user