Compare commits
2 Commits
fcab1c83b7
...
b9aa6e07a9
| Author | SHA1 | Date | |
|---|---|---|---|
| b9aa6e07a9 | |||
| c635d64141 |
@@ -20,6 +20,9 @@ Admin panel for the chocomae service (mouse-typing), extracted into a standalone
|
|||||||
- Upgrade approval sets `AvailableActivateDateTime = NOW() + 1 year`. **Do not change `PlayerCount`** on upgrade.
|
- Upgrade approval sets `AvailableActivateDateTime = NOW() + 1 year`. **Do not change `PlayerCount`** on upgrade.
|
||||||
- When approving an extension or upgrade, close all other `Status = 1` requests for the same `MaestroID` by setting them to `Status = 2`, then set the approved request to `Status = 3`.
|
- When approving an extension or upgrade, close all other `Status = 1` requests for the same `MaestroID` by setting them to `Status = 2`, then set the approved request to `Status = 3`.
|
||||||
- **Docker containers must set `TZ: Asia/Seoul`** so that date calculations (`setHours(0,0,0,0)`, `formatDate`) match the KST-based existing data in the shared MariaDB.
|
- **Docker containers must set `TZ: Asia/Seoul`** so that date calculations (`setHours(0,0,0,0)`, `formatDate`) match the KST-based existing data in the shared MariaDB.
|
||||||
|
- **Docker service names must be unique per environment**: Docker registers each `services:` key as a DNS alias in shared networks. The stage service is `stage-chocoadmin` — **never rename it to `chocoadmin`**. Identical names cause DNS round-robin in `proxy-network`; NPM's `set $server "chocoadmin"` will randomly route requests to either container, mixing production and stage traffic.
|
||||||
|
- **Server actions must be module-level named exports**: Inline `action={async () => { "use server"; ... }}` closures get a new action ID on every build. After redeployment the browser sends the stale ID and Next.js responds `Failed to find Server Action`. Always extract to a top-level export in a separate `actions.ts` file (e.g. `app/(admin)/actions.ts`).
|
||||||
|
- **After redeployment, reload NPM nginx**: Run `docker exec npm nginx -s reload` on the NAS to flush the upstream DNS cache. Without this NPM may continue routing to the previous container's IP.
|
||||||
- **Extension requests** (`POST /api/maestros/[id]/extension-requests`) require `ActivateStatus = 2` (ACTIVE). Trial (1) and cancelled (100) accounts are rejected with 400. Approval (`PATCH /api/extension-requests/[id]`) re-checks `ActivateStatus` and throws 409 if not ACTIVE (transaction rolls back the atomic claim).
|
- **Extension requests** (`POST /api/maestros/[id]/extension-requests`) require `ActivateStatus = 2` (ACTIVE). Trial (1) and cancelled (100) accounts are rejected with 400. Approval (`PATCH /api/extension-requests/[id]`) re-checks `ActivateStatus` and throws 409 if not ACTIVE (transaction rolls back the atomic claim).
|
||||||
- **Upgrade requests**: TRIAL accounts (`ActivateStatus = 1`) may request the **same tier** (trial-to-paid conversion, `requestedAccountType >= AccountType`). ACTIVE accounts must use a strictly higher tier (`>`). At approval time, this direction is re-verified against the maestro's **current** `AccountType` — if it became invalid (e.g., admin manually raised the tier), the approval throws 409 and rolls back.
|
- **Upgrade requests**: TRIAL accounts (`ActivateStatus = 1`) may request the **same tier** (trial-to-paid conversion, `requestedAccountType >= AccountType`). ACTIVE accounts must use a strictly higher tier (`>`). At approval time, this direction is re-verified against the maestro's **current** `AccountType` — if it became invalid (e.g., admin manually raised the tier), the approval throws 409 and rolls back.
|
||||||
- Admin-initiated extension/upgrade registrations do **not** send an email (unlike self-registration flows which send payment-instruction emails). This is intentional.
|
- Admin-initiated extension/upgrade registrations do **not** send an email (unlike self-registration flows which send payment-instruction emails). This is intentional.
|
||||||
@@ -32,6 +35,8 @@ Admin panel for the chocomae service (mouse-typing), extracted into a standalone
|
|||||||
- API routes must call `auth()` and return 401 if no session.
|
- API routes must call `auth()` and return 401 if no session.
|
||||||
- Session `maxAge` is 8 hours — do not lengthen without review.
|
- Session `maxAge` is 8 hours — do not lengthen without review.
|
||||||
- `auth.ts` tracks failed login attempts per admin name in an in-memory `Map`. After 5 consecutive failures the account is locked out for 15 minutes. Counter clears on success.
|
- `auth.ts` tracks failed login attempts per admin name in an in-memory `Map`. After 5 consecutive failures the account is locked out for 15 minutes. Counter clears on success.
|
||||||
|
- Session cookie name `${APP_ENV}-chocoadmin.session-token` must be set in **both** `auth.ts` (`cookies.sessionToken.name`) and `proxy.ts` (`getToken({ cookieName })`). Setting only `auth.ts` causes `proxy.ts` to look for the default cookie name, which is never written → middleware redirects every request to `/login` → login redirects back to `/` → infinite `ERR_TOO_MANY_REDIRECTS`. In NextAuth v5 the JWT encryption salt equals the cookie name, so both must match exactly.
|
||||||
|
- Do **not** add `__Secure-` prefix to the cookie name. Next.js 16 middleware runs in Node.js runtime (not Edge) and receives HTTP from Nginx Proxy Manager — `__Secure-` cookies are silently rejected over HTTP, producing the same redirect loop.
|
||||||
|
|
||||||
## Key files
|
## Key files
|
||||||
|
|
||||||
|
|||||||
+93
-17
@@ -7,7 +7,7 @@ Create environment files on the Synology NAS. Do not commit real `.env.*` files.
|
|||||||
Stage file path:
|
Stage file path:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
/volume1/docker/service/jinaju/stage-chocoadmin/.env.stage
|
/volume1/docker/service/jinaju/chocoadmin/.env.stage
|
||||||
```
|
```
|
||||||
|
|
||||||
Stage example:
|
Stage example:
|
||||||
@@ -20,19 +20,27 @@ AUTH_URL="https://stage-chocoadmin.jinaju.com"
|
|||||||
NEXTAUTH_URL="https://stage-chocoadmin.jinaju.com"
|
NEXTAUTH_URL="https://stage-chocoadmin.jinaju.com"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Production file path:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/volume1/docker/service/jinaju/chocoadmin/.env.production
|
||||||
|
```
|
||||||
|
|
||||||
Production example:
|
Production example:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
DATABASE_URL="mysql://CHCOCO_ADMIN_USER:PASSWORD@AWS_EC2_HOST:3306/chocomae"
|
DATABASE_URL="mysql://CHCOCO_ADMIN_USER:PASSWORD@chocomae.jinaju.com:3306/chocomae"
|
||||||
AUTH_SECRET="replace-with-production-secret"
|
AUTH_SECRET="replace-with-production-secret"
|
||||||
NEXTAUTH_SECRET="replace-with-production-secret"
|
NEXTAUTH_SECRET="replace-with-production-secret"
|
||||||
AUTH_URL="https://NAS_HOST_OR_DOMAIN"
|
AUTH_URL="https://chocoadmin.jinaju.com"
|
||||||
NEXTAUTH_URL="https://NAS_HOST_OR_DOMAIN"
|
NEXTAUTH_URL="https://chocoadmin.jinaju.com"
|
||||||
```
|
```
|
||||||
|
|
||||||
`AUTH_SECRET` and `NEXTAUTH_SECRET` should use the same strong random value per environment for Auth.js compatibility.
|
`AUTH_SECRET` and `NEXTAUTH_SECRET` must be the same strong random value within each environment for Auth.js compatibility. Use different values between production and stage.
|
||||||
|
|
||||||
When the public URL uses HTTPS, both `AUTH_URL` and `NEXTAUTH_URL` must use the exact external HTTPS URL. Auth.js then writes secure cookies, and `proxy.ts` reads the secure session cookie.
|
`APP_ENV` (`production` / `stage`) is set by the `docker-compose*.yml` `environment` block — do **not** put it in the env file. It drives the session cookie name (`${APP_ENV}-chocoadmin.session-token`), which keeps production and stage cookies separate even if they share a browser.
|
||||||
|
|
||||||
|
When the public URL uses HTTPS, both `AUTH_URL` and `NEXTAUTH_URL` must use the exact external HTTPS URL.
|
||||||
|
|
||||||
Generate a secret on the NAS:
|
Generate a secret on the NAS:
|
||||||
|
|
||||||
@@ -42,7 +50,7 @@ openssl rand -base64 32
|
|||||||
|
|
||||||
## 2. Synology Stage Deployment
|
## 2. Synology Stage Deployment
|
||||||
|
|
||||||
The stage source is deployed by pulling Git on the NAS:
|
Both production and stage share the same Git checkout at `/volume1/docker/service/jinaju/chocoadmin`. Pull from there:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /volume1/docker/service/jinaju/stage-chocoadmin
|
cd /volume1/docker/service/jinaju/stage-chocoadmin
|
||||||
@@ -60,12 +68,19 @@ docker network inspect proxy-network >/dev/null 2>&1 || docker network create pr
|
|||||||
Start or update stage:
|
Start or update stage:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f docker-compose.stage.yml down
|
|
||||||
docker compose -f docker-compose.stage.yml up -d --build
|
docker compose -f docker-compose.stage.yml up -d --build
|
||||||
docker compose -f docker-compose.stage.yml ps
|
docker compose -f docker-compose.stage.yml ps
|
||||||
docker compose -f docker-compose.stage.yml logs -f
|
docker compose -f docker-compose.stage.yml logs -f
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If a service was renamed in the compose file, add `--remove-orphans` once to remove the stale container.
|
||||||
|
|
||||||
|
After redeployment, reload NPM to flush its upstream DNS cache:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec npm nginx -s reload
|
||||||
|
```
|
||||||
|
|
||||||
`docker-compose.stage.yml` exposes container port `3000` to `proxy-network` instead of binding host port `3000`, because host port `3000` may already be used by another service such as Gitea.
|
`docker-compose.stage.yml` exposes container port `3000` to `proxy-network` instead of binding host port `3000`, because host port `3000` may already be used by another service such as Gitea.
|
||||||
|
|
||||||
Nginx Proxy Manager settings:
|
Nginx Proxy Manager settings:
|
||||||
@@ -97,7 +112,26 @@ docker compose ps
|
|||||||
docker compose logs -f chocoadmin
|
docker compose logs -f chocoadmin
|
||||||
```
|
```
|
||||||
|
|
||||||
If production is also routed through Nginx Proxy Manager, attach it to `proxy-network` and use the container name and port `3000` as the upstream target.
|
After redeployment, reload NPM:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec npm nginx -s reload
|
||||||
|
```
|
||||||
|
|
||||||
|
Nginx Proxy Manager settings:
|
||||||
|
|
||||||
|
- Scheme: `http`
|
||||||
|
- Forward Hostname / IP: `chocoadmin`
|
||||||
|
- Forward Port: `3000`
|
||||||
|
- Websockets Support: enabled
|
||||||
|
- SSL: enabled
|
||||||
|
- Force SSL: enabled
|
||||||
|
|
||||||
|
Production URL:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://chocoadmin.jinaju.com
|
||||||
|
```
|
||||||
|
|
||||||
The Docker build uses placeholder build-time environment variables only so Next.js can compile without committing secrets. Runtime values are read from the compose `env_file`.
|
The Docker build uses placeholder build-time environment variables only so Next.js can compile without committing secrets. Runtime values are read from the compose `env_file`.
|
||||||
|
|
||||||
@@ -125,11 +159,11 @@ Before enabling approval/rejection operations against production DB:
|
|||||||
|
|
||||||
## 6. Smoke Checks
|
## 6. Smoke Checks
|
||||||
|
|
||||||
After stage container start:
|
After each deployment:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f docker-compose.stage.yml ps
|
docker compose -f docker-compose.stage.yml ps # stage
|
||||||
docker compose -f docker-compose.stage.yml logs -f
|
docker compose ps # production
|
||||||
```
|
```
|
||||||
|
|
||||||
Verify these routes in the browser:
|
Verify these routes in the browser:
|
||||||
@@ -139,9 +173,51 @@ Verify these routes in the browser:
|
|||||||
- `/extension-requests`
|
- `/extension-requests`
|
||||||
- `/upgrade-requests`
|
- `/upgrade-requests`
|
||||||
|
|
||||||
If the browser reports too many redirects after login:
|
## 7. Troubleshooting
|
||||||
|
|
||||||
1. Confirm `AUTH_URL` and `NEXTAUTH_URL` exactly match the public URL and scheme.
|
### ERR_TOO_MANY_REDIRECTS after login
|
||||||
2. Confirm Nginx Proxy Manager forwards to `stage-chocoadmin:3000` with scheme `http`.
|
|
||||||
3. Recreate the container after changing `.env.stage`.
|
Middleware redirects to `/login`, login page redirects back to `/` — infinite loop. Work through this checklist in order:
|
||||||
4. Clear cookies for the stage domain.
|
|
||||||
|
1. **Check `AUTH_URL` / `NEXTAUTH_URL`** — must exactly match the public HTTPS URL including scheme.
|
||||||
|
2. **Check NPM scheme** — NPM must forward with scheme `http` (not `https`) to the container. The app detects HTTPS from `AUTH_URL`, not from the incoming request.
|
||||||
|
3. **Check cookie name consistency** — `auth.ts` (`cookies.sessionToken.name`) and `proxy.ts` (`getToken({ cookieName })`) must both use the same `${APP_ENV}-chocoadmin.session-token` value. If `proxy.ts` uses the default name while `auth.ts` uses a custom one, middleware never finds the session.
|
||||||
|
4. **Check for `__Secure-` prefix** — do not add it. Middleware runs in Node.js runtime and receives HTTP from Nginx. `__Secure-` cookies are silently rejected over HTTP.
|
||||||
|
5. **Clear browser cookies** for the domain, then test in a private window.
|
||||||
|
6. **Recreate the container** after changing `.env.*`.
|
||||||
|
|
||||||
|
### Production and stage traffic mixing
|
||||||
|
|
||||||
|
Symptom: logging into `chocoadmin.jinaju.com` shows the stage UI, or requests hit both containers interchangeably.
|
||||||
|
|
||||||
|
Cause: Docker registers each `services:` key as a DNS alias in `proxy-network`. If stage and production share the same service name, NPM's upstream resolves to both containers via round-robin.
|
||||||
|
|
||||||
|
Check the network:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker network inspect proxy-network --format '{{range .Containers}}{{.Name}} {{.IPv4Address}}{{"\n"}}{{end}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `chocoadmin` and `stage-chocoadmin` appear with different IPs. If `chocoadmin` appears twice, a stale container is using that alias.
|
||||||
|
|
||||||
|
Fix: ensure `docker-compose.stage.yml` has `services: stage-chocoadmin:` (not `chocoadmin`), redeploy stage once with `--remove-orphans` to clean up the stale container, then reload NPM.
|
||||||
|
|
||||||
|
### Failed to find Server Action after redeployment
|
||||||
|
|
||||||
|
Symptom: clicking a button (e.g. logout) returns a 404 or `Failed to find Server Action` error after deploying a new build.
|
||||||
|
|
||||||
|
Cause: the action was defined as an inline `"use server"` closure. Closures get a new action ID on every build. The browser cached the old ID.
|
||||||
|
|
||||||
|
Fix: force-reload the page (`Cmd+Shift+R` / `Ctrl+Shift+R`) to discard the cached page with stale action IDs. If the problem recurs after every deployment, the action must be extracted to a module-level named export in a separate `actions.ts` file.
|
||||||
|
|
||||||
|
### Container starts but immediately exits
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose logs chocoadmin
|
||||||
|
```
|
||||||
|
|
||||||
|
Common causes:
|
||||||
|
|
||||||
|
- Missing `DATABASE_URL` or malformed connection string.
|
||||||
|
- `AUTH_SECRET` not set — NextAuth throws on startup.
|
||||||
|
- Port already allocated — check if another service uses host port 3000. Both compose files use `expose` (not `ports`) for port 3000, so this should not happen unless the compose file was modified.
|
||||||
|
|||||||
@@ -388,9 +388,9 @@ chocoadmin/
|
|||||||
|
|
||||||
- [x] Dockerfile (프로덕션용 pnpm 빌드)
|
- [x] Dockerfile (프로덕션용 pnpm 빌드)
|
||||||
- [x] `docker-compose.yml` (Synology NAS Container Manager용)
|
- [x] `docker-compose.yml` (Synology NAS Container Manager용)
|
||||||
- [ ] AWS EC2 보안그룹 인바운드 설정 (NAS IP → 3306 허용) — `docs/deployment.md`에 절차 문서화
|
- [x] AWS EC2 보안그룹 인바운드 설정 (NAS IP → 3306 허용)
|
||||||
- [ ] Synology NAS에서 컨테이너 실행 테스트 — `docs/deployment.md`에 절차 문서화
|
- [x] Synology NAS에서 컨테이너 실행 확인 (production + stage 동시 운영 중)
|
||||||
- [ ] 운영 DB read-only 리허설 후 변경 기능 활성화 — `docs/deployment.md`에 절차 문서화
|
- [x] 운영 DB 연결 및 변경 기능 확인
|
||||||
|
|
||||||
### Phase 8. 검증 및 안정화
|
### Phase 8. 검증 및 안정화
|
||||||
|
|
||||||
@@ -535,6 +535,35 @@ chocoadmin/
|
|||||||
- [x] 기존 `lib/maestros.ts`의 로컬 정의 제거 및 import로 교체
|
- [x] 기존 `lib/maestros.ts`의 로컬 정의 제거 및 import로 교체
|
||||||
- [x] 두 신규 API route의 로컬 정의 제거 및 import로 교체
|
- [x] 두 신규 API route의 로컬 정의 제거 및 import로 교체
|
||||||
|
|
||||||
|
### Phase 13. 보안·정합성 수정
|
||||||
|
|
||||||
|
> AGENTS.md "Implemented phases" Phase 13 항목 참조.
|
||||||
|
|
||||||
|
- [x] 모든 항목 완료 (P0-1 ~ P2-8)
|
||||||
|
|
||||||
|
### Phase 14. Production/Stage 배포 환경 안정화
|
||||||
|
|
||||||
|
> Production 첫 배포 과정에서 발견된 이슈들을 해결했다.
|
||||||
|
> 완료 날짜: 2026-07-05
|
||||||
|
|
||||||
|
- [x] Production 배포 경로 생성 및 git pull 방식 배포 설정
|
||||||
|
- Production: `/volume1/docker/service/jinaju/chocoadmin` + `docker-compose.yml`
|
||||||
|
- Stage: 동일 경로, `-f docker-compose.stage.yml` 로 구분
|
||||||
|
- [x] **포트 충돌 수정** (`docker-compose.yml`)
|
||||||
|
- Gitea가 호스트 3000 포트를 선점 → `ports: "3000:3000"` → `expose: "3000"` 으로 변경
|
||||||
|
- [x] **Docker DNS alias 충돌 수정** (`docker-compose.stage.yml`)
|
||||||
|
- `services: chocoadmin:` → `services: stage-chocoadmin:` 으로 변경
|
||||||
|
- 동일 서비스명은 `proxy-network`에서 같은 DNS alias로 등록됨
|
||||||
|
- NPM의 `set $server "chocoadmin"` 이 두 컨테이너에 라운드로빈 → 트래픽이 production/stage 사이에 무작위 분산
|
||||||
|
- [x] **NextAuth v5 커스텀 쿠키 이름으로 production/stage 쿠키 분리**
|
||||||
|
- `auth.ts`: `cookies.sessionToken.name = "${APP_ENV}-chocoadmin.session-token"` 추가
|
||||||
|
- `proxy.ts`: `getToken({ cookieName })` 추가 — `auth.ts`에만 적용하면 middleware가 세션 토큰을 찾지 못해 `ERR_TOO_MANY_REDIRECTS` 발생
|
||||||
|
- `__Secure-` prefix 금지: Node.js 런타임은 Nginx에서 HTTP로 수신하므로 Secure 쿠키가 무시됨
|
||||||
|
- [x] **서버 액션 인라인 클로저 제거** — 로그아웃 액션을 `app/(admin)/actions.ts` 로 추출
|
||||||
|
- 인라인 `"use server"` 클로저는 빌드마다 새 ID 부여 → 재배포 후 `Failed to find Server Action`
|
||||||
|
- 코드베이스 전체 인라인 서버 액션 검토 및 동일 패턴 수정 완료
|
||||||
|
- [x] Production/Stage 동시 운영 정상 확인
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. MVP 범위
|
## 10. MVP 범위
|
||||||
|
|||||||
@@ -47,24 +47,30 @@ function inlineParams(query: string, rawParams: string): string {
|
|||||||
|
|
||||||
function onQuery(e: Prisma.QueryEvent): void {
|
function onQuery(e: Prisma.QueryEvent): void {
|
||||||
const isSlow = e.duration >= SLOW_QUERY_MS;
|
const isSlow = e.duration >= SLOW_QUERY_MS;
|
||||||
const level = isSlow ? "warn" : "debug";
|
|
||||||
if (!logger.isEnabled(level)) return;
|
|
||||||
|
|
||||||
const isSensitive = e.query.includes("PASSWORD(");
|
const isSensitive = e.query.includes("PASSWORD(");
|
||||||
const sqlToFormat = isSensitive ? e.query : inlineParams(e.query, e.params);
|
const sqlWithParams = isSensitive ? e.query : inlineParams(e.query, e.params);
|
||||||
const formatted = format(sqlToFormat, { language: "mysql", tabWidth: 2 });
|
|
||||||
|
|
||||||
const ts = timestamp();
|
if (isSlow) {
|
||||||
const tag = level.toUpperCase().padEnd(5);
|
const formatted = format(sqlWithParams, { language: "mysql", tabWidth: 2 });
|
||||||
const slowLabel = isSlow ? " [SLOW]" : "";
|
const ts = timestamp();
|
||||||
const header = `[${ts}] [${tag}] [db:query] ${e.duration}ms${slowLabel}`;
|
const indented = formatted.split("\n").map((l) => ` ${l}`).join("\n");
|
||||||
const indented = formatted
|
writeAll(2, `[${ts}] [WARN ] [db:query] ${e.duration}ms [SLOW]\n${indented}\n`);
|
||||||
.split("\n")
|
return;
|
||||||
.map((l) => ` ${l}`)
|
}
|
||||||
.join("\n");
|
|
||||||
|
|
||||||
const out = `${header}\n${indented}\n`;
|
if (logger.isEnabled("debug")) {
|
||||||
writeAll(isSlow ? 2 : 1, out);
|
const formatted = format(sqlWithParams, { language: "mysql", tabWidth: 2 });
|
||||||
|
const ts = timestamp();
|
||||||
|
const indented = formatted.split("\n").map((l) => ` ${l}`).join("\n");
|
||||||
|
writeAll(1, `[${ts}] [DEBUG] [db:query] ${e.duration}ms\n${indented}\n`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logger.isEnabled("info")) {
|
||||||
|
const sql = sqlWithParams.replace(/\s+/g, " ").trim();
|
||||||
|
const ts = timestamp();
|
||||||
|
writeAll(1, `[${ts}] [INFO ] [db:query] ${e.duration}ms ${sql}\n`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 결과 로깅 ($extends) ──────────────────────────────────────────
|
// ── 결과 로깅 ($extends) ──────────────────────────────────────────
|
||||||
@@ -234,9 +240,17 @@ function createPrismaClient() {
|
|||||||
const extendedClient = baseClient.$extends({
|
const extendedClient = baseClient.$extends({
|
||||||
query: {
|
query: {
|
||||||
$allOperations: async ({ model, operation, args, query }) => {
|
$allOperations: async ({ model, operation, args, query }) => {
|
||||||
const result = await query(args);
|
try {
|
||||||
logResult(model, operation, result);
|
const result = await query(args);
|
||||||
return result;
|
logResult(model, operation, result);
|
||||||
|
return result;
|
||||||
|
} catch (err) {
|
||||||
|
const label = model ? `${model}.${operation}` : operation;
|
||||||
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
|
const ts = timestamp();
|
||||||
|
writeAll(2, `[${ts}] [ERROR] [db:error] ${label} failed: ${msg}\n`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user