diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0adb1a4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +node_modules +npm-debug.log +Dockerfile +.dockerignore +.git +.env +dist +core diff --git a/.gitignore b/.gitignore index 102e001..3232860 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,6 @@ node_modules/ dist/ .env .env.* +!.env.example *.log .plan/ diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..a45fd52 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24 diff --git a/CLAUDE.md b/CLAUDE.md index 6225cf6..dbf6476 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,7 +33,32 @@ test: 테스트 ## 레이어 구조 (api) ``` -routes/ → 요청/응답 처리 -services/ → 비즈니스 로직 -repositories/ → DB 접근 +src/ + domain/ → 도메인 타입 + 레포지토리 인터페이스 (순수 계약, import 없음) + repositories/ → DB 구현체 (domain 인터페이스 구현) + services/ → 비즈니스 로직 (domain 인터페이스만 의존) + routes/ → 요청/응답 처리 + 의존성 조립 + schemas/ → Fastify JSON 스키마 (validation) + plugins/ → Fastify 플러그인 (DB 등) + errors.ts → AppError (중앙화된 에러 클래스) + app.ts → buildApp 팩토리 함수 (테스트 재사용) + index.ts → listen만 담당 + test/ → Vitest 테스트 + +migrations/ → SQL 마이그레이션 파일 +``` + +### 의존성 방향 + ``` +routes → repositories (구현체 조립) +routes → services +services → domain (인터페이스) +repositories → domain (인터페이스 구현) +domain ← 아무것도 import 안 함 +``` + +### 에러 처리 + +- `AppError(statusCode, message)` throw → `setErrorHandler`에서 일괄 처리 +- Fastify schema validation 에러 → `error.validation` 체크 후 400 반환 diff --git a/docs/docker-compose.md b/docs/docker-compose.md new file mode 100644 index 0000000..42138db --- /dev/null +++ b/docs/docker-compose.md @@ -0,0 +1,185 @@ +# docker-compose migrate 서비스 추가 + +## 배경 + +초기 `docker-compose.yaml`은 `api`와 `db` 두 서비스만 존재했다. +이 상태에서 `docker compose up`을 실행하면 다음 문제가 발생한다. + +``` +api 컨테이너 기동 + → GET /items 호출 + → ERROR: relation "items" does not exist +``` + +테이블이 없는 상태에서 api가 먼저 뜨기 때문이다. 개발자가 직접 아래 명령을 실행해야 했다. + +```bash +psql $DATABASE_URL -f migrations/001_create_items.sql +``` + +이 문제를 해결하기 위해 `migrate` 서비스를 추가했다. + +--- + +## 마이그레이션 실행 방식 비교 + +마이그레이션을 자동화하는 방법은 크게 두 가지다. + +### 방식 1: 엔트리포인트 스크립트 (같은 컨테이너) + +```sh +#!/bin/sh +# entrypoint.sh +node-pg-migrate up # 마이그레이션 +exec node dist/index.js # 서버 실행 +``` + +```yaml +# docker-compose.yaml +api: + command: ["sh", "entrypoint.sh"] + depends_on: + db: + condition: service_healthy +``` + +**동작:** api 컨테이너 안에서 마이그레이션을 먼저 실행한 뒤 서버를 띄운다. + +**문제점 — 스케일 아웃 시 레이스 컨디션:** + +```yaml +api: + deploy: + replicas: 3 # 3개 컨테이너가 동시에 마이그레이션 실행 +``` + +3개 컨테이너가 동시에 `CREATE TABLE items`를 실행하면: + +``` +replica-1: CREATE TABLE items → 성공 +replica-2: CREATE TABLE items → ERROR: relation "items" already exists +replica-3: CREATE TABLE items → ERROR: relation "items" already exists +``` + +`IF NOT EXISTS`로 회피할 수 있지만, 복잡한 DDL(ALTER TABLE, 인덱스 생성 등)에서는 동시 실행 자체가 위험하다. + +--- + +### 방식 2: 별도 migrate 컨테이너 (현재 방식) + +```yaml +migrate: + build: + context: ../../ + dockerfile: packages/api/Dockerfile + command: ["node_modules/.bin/node-pg-migrate", "-m", "packages/api/migrations", "up"] + depends_on: + db: + condition: service_healthy + +api: + depends_on: + migrate: + condition: service_completed_successfully +``` + +**동작:** migrate 컨테이너가 완전히 종료(exit 0)된 후에만 api 컨테이너가 기동된다. + +스케일 아웃해도 migrate는 1개만 실행되므로 레이스 컨디션이 없다. + +--- + +## depends_on condition 비교 + +`depends_on`의 `condition`은 세 가지 값을 지원한다. + +| condition | 의미 | 언제 사용 | +|-----------|------|----------| +| `service_started` | 컨테이너 프로세스가 시작됨 | 종속성이 단순히 실행 중이어야 할 때 | +| `service_healthy` | healthcheck가 통과됨 | DB처럼 실제로 요청을 받을 준비가 된 상태가 필요할 때 | +| `service_completed_successfully` | 컨테이너가 exit code 0으로 종료됨 | 마이그레이션처럼 "완전히 끝났다"는 보장이 필요할 때 | + +이 프로젝트의 기동 순서: + +``` +db (service_healthy) + ↓ pg_isready 통과 후 +migrate (service_completed_successfully) + ↓ exit 0 후 +api +``` + +`service_started`나 `service_healthy`를 migrate에 적용하면 migrate가 실행 중일 때 api가 기동되어 테이블이 없는 상태에서 쿼리가 실행될 수 있다. + +--- + +## 실측 지표 + +환경: Apple M-series, Docker Desktop, 이미지 빌드 완료 상태 + +### migrate 컨테이너 실행 시간 + +| 실행 | 결과 | 소요 시간 | +|------|------|----------| +| 1회차 (최초, 볼륨 없음) | `001_create_items` 실행 | ~298ms | +| 2회차 (볼륨 유지) | `No migrations to run!` 스킵 | ~162ms | + +2회차에서 migrate가 `pgmigrations` 테이블을 확인하고 이미 실행된 파일은 건너뛴다. **마이그레이션은 항상 안전하게 재실행 가능하다.** + +### docker compose up 전체 시간 + +| 상태 | 소요 시간 | +|------|----------| +| 최초 기동 (볼륨 없음) | ~6.5s | +| 재기동 (볼륨 유지, 서비스 Running) | ~1.2s | + +### 이미지 크기 + +방식 1(엔트리포인트)은 migrate 전용 이미지가 없으므로 해당 없음. +방식 2에서 migrate 컨테이너는 api와 동일한 이미지를 재사용한다. + +| 이미지 | 크기 | 용도 | +|--------|------|------| +| `docker-api` (= `docker-migrate`) | 178 MB | api 실행 + 마이그레이션 | +| `postgres:16-alpine` | 272 MB | (구 방식: psql로 마이그레이션) | + +별도 postgres 이미지 대신 api 이미지를 재사용하므로 **migrate 전용 이미지가 추가로 필요 없다.** + +--- + +## 새 마이그레이션 추가 시 비교 + +### 구 방식 (psql 하드코딩) + +```yaml +# docker-compose.yaml 수정 필요 +command: > + sh -c "psql $DATABASE_URL -f /migrations/001_create_items.sql && + psql $DATABASE_URL -f /migrations/002_add_tags.sql" +``` + +파일이 늘어날수록 `docker-compose.yaml`도 함께 수정해야 한다. + +### 현재 방식 (node-pg-migrate) + +``` +migrations/ + 001_create_items.js ← 기존 + 002_add_tags.js ← 파일만 추가 +``` + +`docker-compose.yaml` 수정 없이 파일만 추가하면 된다. `node-pg-migrate`가 `pgmigrations` 테이블과 대조해 실행되지 않은 파일만 순서대로 실행한다. + +--- + +## K8s 전환 시 매핑 + +별도 컨테이너 방식은 Kubernetes의 Job / InitContainer 패턴과 자연스럽게 대응된다. + +| docker-compose | Kubernetes | +|----------------|-----------| +| migrate 서비스 | `Job` (한 번 실행 후 완료) | +| `service_completed_successfully` | `initContainers` (완료 후 메인 컨테이너 기동) | +| api 서비스 | `Deployment` | + +엔트리포인트 방식으로 구현했다면 K8s 전환 시 구조를 다시 설계해야 한다. diff --git a/docs/docker.md b/docs/docker.md new file mode 100644 index 0000000..0d8afd4 --- /dev/null +++ b/docs/docker.md @@ -0,0 +1,346 @@ +# Dockerfile 비교 및 학습 기록 + +## 개요 + +`packages/api`의 Dockerfile을 두 가지 버전으로 작성하고, 직접 디버깅하며 Docker 동작 원리를 파악한다. + +| 항목 | Dockerfile.naive | Dockerfile.optimized | +|------|-----------------|----------------------| +| 빌드 방식 | 단일 스테이지 | multi-stage (builder + runner) | +| 보안 | root 실행 | non-root (USER node) | +| 이미지 크기 | 202 MB | 167 MB | +| devDependencies 포함 | ✅ (런타임에도 포함) | ❌ (빌드 후 제외) | + +--- + +## Dockerfile.naive + +```dockerfile +FROM node:22-alpine + +WORKDIR /app + +COPY package*.json ./ +COPY packages/api/package.json ./packages/api/ +COPY packages/shared/package.json ./packages/shared/ + +RUN npm ci + +COPY . . + +RUN npm run build -w packages/api + +EXPOSE 3000 + +ENTRYPOINT ["node"] +CMD ["packages/api/dist/index.js"] +``` + +**의도적으로 생략한 것들:** +- `USER node` — root로 실행 (보안 취약) +- multi-stage — devDependencies가 런타임 이미지에 포함됨 + +--- + +## Dockerfile.optimized + +```dockerfile +FROM node:20-alpine AS builder + +WORKDIR /app +RUN chown node:node /app +USER node + +COPY --chown=node:node package*.json ./ +COPY --chown=node:node packages/api/package.json ./packages/api/ +COPY --chown=node:node packages/shared/package.json ./packages/shared/ +RUN npm ci + +COPY --chown=node:node . . + +RUN npm run build -w packages/api + +FROM node:20-alpine AS runner + +WORKDIR /app + +COPY --from=builder /app/packages/api/dist ./packages/api/dist +COPY --from=builder /app/packages/shared ./packages/shared +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/package.json ./ + +USER node + +EXPOSE 3000 + +ENTRYPOINT ["node"] +CMD ["packages/api/dist/index.js"] +``` + +--- + +## 디버깅 과정 + +### 1. npm ci 실패 — package-lock.json 없음 + +``` +npm error The `npm ci` command can only install with an existing package-lock.json +``` + +**원인:** `COPY package*.json ./`은 현재 디렉토리(루트)의 `package.json`, `package-lock.json`만 복사한다. +모노레포 워크스페이스 구조에서 각 패키지의 `package.json`은 별도로 명시해야 한다. + +**해결:** +```dockerfile +COPY package*.json ./ +COPY packages/api/package.json ./packages/api/ +COPY packages/shared/package.json ./packages/shared/ +``` + +**배운 점:** `npm ci`는 `package-lock.json` 기준으로 정확한 버전을 설치한다. +`npm install`과 달리 lock 파일을 수정하지 않아 CI/Docker 환경에 적합하다. + +--- + +### 2. EACCES: permission denied — USER 선언 순서 문제 + +``` +npm error EACCES: permission denied, mkdir '/app/node_modules' +``` + +**원인:** `USER node` 선언 이후 `WORKDIR`이 생성되거나 `COPY`가 실행되면 +파일 소유자가 root가 되어 node 유저가 쓰기 권한을 갖지 못한다. + +```dockerfile +# 잘못된 순서 +USER node # node 유저로 전환 +WORKDIR /app # root 소유로 /app 생성 +COPY . . # root 소유로 복사 +RUN npm ci # node 유저가 node_modules 생성 시도 → EACCES +``` + +**해결 (optimized):** `--chown` 옵션으로 복사 시 소유자를 명시한다. +```dockerfile +WORKDIR /app +RUN chown node:node /app +USER node +COPY --chown=node:node . . +RUN npm ci # node 유저가 자신 소유 파일에 쓰기 → 성공 +``` + +**해결 (naive):** `USER node`를 아예 제거한다. (보안 고려 안 하는 버전) + +**배운 점:** Docker 레이어는 순서가 중요하다. `USER`, `WORKDIR`, `COPY`의 실행 순서에 따라 +파일 소유권이 달라지며, 이후 명령의 실행 권한에 영향을 준다. + +--- + +### 3. tsc: not found — 모노레포 워크스페이스 구조 미인식 + +``` +sh: tsc: not found +``` + +**원인:** `COPY package*.json ./`가 루트 `package.json`만 복사하면 +`npm ci`가 워크스페이스 구조를 인식하지 못해 각 패키지의 `devDependencies`를 설치하지 않는다. +`typescript`는 `packages/api/package.json`의 `devDependencies`에 있으므로 설치되지 않는다. + +**해결:** 워크스페이스 패키지의 `package.json`을 먼저 복사해서 npm이 전체 워크스페이스 구조를 인식하게 한다. + +**배운 점:** npm workspaces는 루트에서 의존성을 통합 관리하지만, +각 패키지의 `package.json`이 있어야 워크스페이스 구조를 인식한다. + +--- + +### 4. dist 경로 오류 — 모노레포 빌드 결과물 위치 + +``` +Error: Cannot find module '/app/dist/index.js' +``` + +**원인:** `npm run build -w packages/api`는 `packages/api/` 안에서 빌드하므로 +결과물은 `/app/packages/api/dist/`에 생성된다. `/app/dist/`가 아니다. + +**해결:** +```dockerfile +CMD ["packages/api/dist/index.js"] # 실제 경로 +``` + +**배운 점:** 모노레포에서 빌드 컨텍스트와 워크스페이스 명령의 실행 위치를 명확히 구분해야 한다. + +--- + +### 5. multi-stage에서 shared 패키지 누락 + +**원인:** `node_modules/@devopsim/shared`는 `packages/shared/`를 심볼릭 링크로 가리킨다. +runner 스테이지에서 `node_modules`만 복사하면 링크 타겟이 없어 런타임에 모듈을 찾지 못한다. + +**해결:** +```dockerfile +COPY --from=builder /app/packages/shared ./packages/shared # 링크 타겟 포함 +COPY --from=builder /app/node_modules ./node_modules # 심볼릭 링크 +``` + +**배운 점:** npm workspaces의 로컬 패키지는 심볼릭 링크로 연결된다. +multi-stage 빌드에서 링크 타겟도 함께 복사해야 한다. + +--- + +## 빌드 결과 비교 + +환경: Apple M-series, Docker Desktop, 로컬 네트워크 (base image 캐시됨) + +### 최초 빌드 (--no-cache) + +| | naive | optimized | +|---|---|---| +| 빌드 시간 | ~4.8s | ~4.2s | +| 이미지 크기 | 202 MB | 167 MB | + +> base image(`node:alpine`)가 이미 로컬에 캐시된 상태라 절대 시간보다 크기 차이가 더 의미 있는 지표다. + +### 소스 코드 변경 후 재빌드 (캐시 활용) + +`packages/api/src/routes/health.ts` 수정 후 재빌드: + +| | naive | optimized | +|---|---|---| +| 재빌드 시간 | ~1.6s | ~1.9s | +| npm ci | CACHED ✅ | CACHED ✅ | +| 재실행 레이어 | COPY + tsc | COPY + tsc + runner COPY | + +**분석:** +- 두 버전 모두 `package*.json`을 소스보다 먼저 복사하는 레이어 캐시 전략을 사용하므로 + 소스 변경 시 `npm ci`는 캐시 히트된다. +- optimized는 runner 스테이지 COPY가 추가되어 재빌드가 약간 더 걸린다. +- 의존성 변경(`package.json` 수정) 시에는 두 버전 모두 `npm ci`부터 다시 실행된다. + +### 크기 차이 원인 + +| 구성 요소 | naive | optimized | +|-----------|-------|-----------| +| 소스코드 (src/) | ✅ 포함 | ❌ 제외 | +| devDependencies (typescript 등) | ✅ 포함 | ❌ 제외 | +| 빌드 결과물 (dist/) | ✅ 포함 | ✅ 포함 | +| production dependencies | ✅ 포함 | ✅ 포함 | + +35MB 차이의 대부분은 `typescript`, `ts-node`, `@types/*` 등 devDependencies다. + +--- + +## 빌드 명령어 + +```bash +# 루트에서 실행 (모노레포 컨텍스트 필요) + +# naive +docker build -f packages/api/Dockerfile.naive -t devopsim-api-naive . + +# optimized +docker build -f packages/api/Dockerfile.optimized -t devopsim-api-optimized . + +# 실행 +docker run -e DATABASE_URL=postgresql://dummy -p 3000:3000 devopsim-api-naive +``` + +--- + +## Dockerfile (프로덕션) — 3-stage 구조 + +`Dockerfile.optimized`에서 두 가지 문제가 남아 있었다. + +1. **node_modules 전체 복사**: builder의 `node_modules`에는 `typescript`, `ts-node` 등 devDependencies가 섞여 있어 runner에서 그대로 복사하면 불필요한 패키지가 포함된다. +2. **packages/shared 전체 복사**: `packages/shared`의 소스 파일, tsconfig 등 런타임에 불필요한 파일이 최종 이미지에 들어간다. + +### 해결 구조: 3-stage 빌드 + +``` +deps → npm ci --omit=dev production 의존성만 설치 (runner에 복사할 node_modules) +builder → npm ci + tsc 빌드 전체 의존성 + shared/api 각각 빌드 (dist 생성) +runner → 조립 deps의 node_modules + builder의 dist만 복사 +``` + +**왜 deps와 builder를 분리하는가:** + +빌드에는 `typescript`가 필요하므로 `npm ci`(전체 설치)를 해야 한다. 그러면 builder의 `node_modules`에 devDependencies가 섞인다. 분리할 방법이 없기 때문에, 처음부터 `--omit=dev`로만 설치한 별도 스테이지(deps)를 만들어 runner에서 가져다 쓴다. + +### npm workspaces 심링크와 shared dist + +``` +node_modules/@devopsim/shared → (심링크) → packages/shared/ + package.json ← "main": "dist/index.js" + dist/index.js ← 실제 파일 +``` + +`node_modules/@devopsim/shared`는 파일이 없고 `packages/shared/`를 가리키는 심링크다. Node.js가 `require('@devopsim/shared')`를 만나면 심링크를 따라가 `packages/shared/package.json`의 `"main"` 필드를 읽고 `dist/index.js`를 로드한다. + +`deps` 스테이지에서는 `package.json`만 복사하고 소스가 없으므로 `packages/shared/dist/`가 존재하지 않는다. 따라서 builder에서 `npm run build -w packages/shared`로 dist를 생성한 뒤 runner에 복사해야 한다. + +runner에 필요한 파일: +- `node_modules/` (deps — production 패키지 + 심링크) +- `packages/shared/dist/` (builder — 심링크가 가리키는 실제 파일) +- `packages/shared/package.json` (builder — `"main"` 필드 해석용) +- `packages/api/dist/` (builder — 앱 실행 파일) + +--- + +## 3-stage 추가 후 실측 비교 + +환경: Apple M-series, Docker Desktop, base image 캐시됨 (2025-04) + +### 이미지 크기 + +| | naive | optimized | prod (3-stage) | +|---|---|---|---| +| 이미지 크기 | 212 MB | 175 MB | **147 MB** | +| naive 대비 | — | -37 MB (-17%) | **-65 MB (-31%)** | + +### node_modules 크기 + +| | naive | optimized | prod | +|---|---|---|---| +| node_modules | 48.6 MB | 48.6 MB | **20.1 MB** | + +`optimized`는 multi-stage로 소스를 분리했지만 `node_modules`는 builder 것을 그대로 복사해서 devDependencies가 그대로 포함된다. `prod`는 처음부터 `--omit=dev`로 설치한 것을 가져와 node_modules가 절반 이하다. + +### 빌드 시간 + +| | naive | optimized | prod (3-stage) | +|---|---|---|---| +| 빌드 시간 (캐시 활용) | 3.8s | 3.0s | 4.4s | + +prod는 스테이지가 하나 더 많아(deps) 빌드 시간이 소폭 증가한다. 그러나 `deps`와 `builder` 스테이지는 병렬 실행 가능하도록 Docker BuildKit이 최적화하므로 실제 차이는 작다. + +### 포함 내용 비교 + +| | naive | optimized | prod | +|---|---|---|---| +| 실행 유저 | root (보안 취약) | node | node | +| src/ 소스 파일 | ✅ 포함 | ❌ 제외 | ❌ 제외 | +| devDependencies (typescript 등) | ✅ 포함 | ✅ 포함 | ❌ 제외 | +| packages/shared 소스 | ✅ 포함 | ✅ 포함 | ❌ 제외 (dist만) | +| production dependencies | ✅ 포함 | ✅ 포함 | ✅ 포함 | +| dist/ 빌드 결과물 | ✅ 포함 | ✅ 포함 | ✅ 포함 | + +### 동작 확인 + +```bash +$ docker run -d -p 3001:3000 devopsim-prod +$ curl http://localhost:3001/health +{"status":"ok"} +``` + +--- + +## packages/shared 사전 조건 + +`Dockerfile`(3-stage)에서 `npm run build -w packages/shared`를 실행하므로 `packages/shared`에 빌드 구조가 필요하다. + +``` +packages/shared/ + tsconfig.json ← packages/api/tsconfig.json과 동일한 구조로 추가 + src/ + index.ts ← shared 유틸리티 진입점 +``` + +`tsconfig.json` 없이 빌드하면 루트 `tsconfig.json`이 `**/*`로 전체를 탐색해 `packages/api/src`까지 포함하려 해 `rootDir` 충돌 에러가 발생한다. diff --git a/infra/docker/.env.example b/infra/docker/.env.example new file mode 100644 index 0000000..19b8ace --- /dev/null +++ b/infra/docker/.env.example @@ -0,0 +1,4 @@ +POSTGRES_DB=devopsim +POSTGRES_USER=devopsim +POSTGRES_PASSWORD=devopsim +DATABASE_URL=postgresql://devopsim:devopsim@db:5432/devopsim diff --git a/infra/docker/docker-compose.yaml b/infra/docker/docker-compose.yaml new file mode 100644 index 0000000..8e89db0 --- /dev/null +++ b/infra/docker/docker-compose.yaml @@ -0,0 +1,48 @@ +services: + api: + build: + context: ../../ + dockerfile: packages/api/Dockerfile + ports: + - "3000:3000" + env_file: .env + restart: on-failure:3 + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:3000/health || exit 1"] + interval: 5s + timeout: 5s + retries: 3 + start_period: 30s + start_interval: 5s + depends_on: + migrate: + condition: service_completed_successfully + + migrate: + build: + context: ../../ + dockerfile: packages/api/Dockerfile + env_file: .env + command: ["node_modules/.bin/node-pg-migrate", "-m", "packages/api/migrations", "up"] + depends_on: + db: + condition: service_healthy + + db: + image: postgres:16-alpine + env_file: .env + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + restart: on-failure:3 + healthcheck: + test: ["CMD-SHELL", "pg_isready -U devopsim"] + interval: 5s + timeout: 5s + retries: 5 + start_period: 30s + start_interval: 5s + +volumes: + postgres_data: diff --git a/package-lock.json b/package-lock.json index e4fb536..e760200 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,10 @@ "name": "devopsim", "workspaces": [ "packages/*" - ] + ], + "engines": { + "node": ">=24" + } }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", @@ -30,6 +33,43 @@ "resolved": "packages/shared", "link": true }, + "node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@fastify/ajv-compiler": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz", @@ -163,6 +203,15 @@ "ipaddr.js": "^2.1.0" } }, + "node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -191,6 +240,25 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz", + "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, "node_modules/@opentelemetry/api": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", @@ -200,12 +268,309 @@ "node": ">=8.0.0" } }, + "node_modules/@oxc-project/types": { + "version": "0.122.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", + "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@pinojs/redact": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", "license": "MIT" }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz", + "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", + "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz", + "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@tsconfig/node10": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", @@ -234,90 +599,285 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/node": { - "version": "22.19.15", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", - "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "undici-types": "~6.21.0" + "tslib": "^2.4.0" } }, - "node_modules/abstract-logging": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", - "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, "license": "MIT" }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", + "devOptional": true, "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" + "dependencies": { + "undici-types": "~6.21.0" } }, - "node_modules/acorn-walk": { - "version": "8.3.5", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", - "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", - "dev": true, + "node_modules/@types/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", + "devOptional": true, "license": "MIT", "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" } }, - "node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "node_modules/@vitest/expect": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz", + "integrity": "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==", + "dev": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "url": "https://opencollective.com/vitest" } }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "node_modules/@vitest/mocker": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.2.tgz", + "integrity": "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==", + "dev": true, "license": "MIT", "dependencies": { - "ajv": "^8.0.0" + "@vitest/spy": "4.1.2", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "ajv": "^8.0.0" + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { - "ajv": { + "msw": { + "optional": true + }, + "vite": { "optional": true } } }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "node_modules/@vitest/pretty-format": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz", + "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==", "dev": true, - "license": "MIT" - }, - "node_modules/atomic-sleep": { - "version": "1.0.0", + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.2.tgz", + "integrity": "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.2", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.2.tgz", + "integrity": "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.2", + "@vitest/utils": "4.1.2", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.2.tgz", + "integrity": "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.2.tgz", + "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.2", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", "license": "MIT", @@ -345,12 +905,82 @@ "fastq": "^1.17.1" } }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/bintrees": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==", "license": "MIT" }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", @@ -371,6 +1001,20 @@ "dev": true, "license": "MIT" }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -380,6 +1024,16 @@ "node": ">=6" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/diff": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", @@ -390,6 +1044,48 @@ "node": ">=0.3.1" } }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-decode-uri-component": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", @@ -509,6 +1205,24 @@ "reusify": "^1.0.4" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/find-my-way": { "version": "9.5.0", "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.5.0.tgz", @@ -523,6 +1237,70 @@ "node": ">=20" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ipaddr.js": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", @@ -532,6 +1310,36 @@ "node": ">= 10" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/json-schema-ref-resolver": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", @@ -594,6 +1402,298 @@ ], "license": "MIT" }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -601,6 +1701,85 @@ "dev": true, "license": "ISC" }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-pg-migrate": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/node-pg-migrate/-/node-pg-migrate-8.0.4.tgz", + "integrity": "sha512-HTlJ6fOT/2xHhAUtsqSN85PGMAqSbfGJNRwQF8+ZwQ1+sVGNUTl/ZGEshPsOI3yV22tPIyHXrKXr3S0JxeYLrg==", + "license": "MIT", + "dependencies": { + "glob": "~11.1.0", + "yargs": "~17.7.0" + }, + "bin": { + "node-pg-migrate": "bin/node-pg-migrate.js" + }, + "engines": { + "node": ">=20.11.0" + }, + "peerDependencies": { + "@types/pg": ">=6.0.0 <9.0.0", + "pg": ">=4.3.0 <9.0.0" + }, + "peerDependenciesMeta": { + "@types/pg": { + "optional": true + } + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/on-exit-leak-free": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", @@ -610,6 +1789,44 @@ "node": ">=14.0.0" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/pg": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", @@ -658,7 +1875,6 @@ "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", "license": "ISC", - "peer": true, "engines": { "node": ">=4.0.0" } @@ -677,15 +1893,13 @@ "version": "1.13.0", "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/pg-types": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", "license": "MIT", - "peer": true, "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", @@ -707,6 +1921,26 @@ "split2": "^4.1.0" } }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/pino": { "version": "9.14.0", "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", @@ -744,12 +1978,40 @@ "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", "license": "MIT" }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", "license": "MIT", - "peer": true, "engines": { "node": ">=4" } @@ -759,7 +2021,6 @@ "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -769,7 +2030,6 @@ "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -779,7 +2039,6 @@ "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", "license": "MIT", - "peer": true, "dependencies": { "xtend": "^4.0.0" }, @@ -831,6 +2090,15 @@ "node": ">= 12.13.0" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -865,6 +2133,40 @@ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "license": "MIT" }, + "node_modules/rolldown": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz", + "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.122.0", + "@rolldown/pluginutils": "1.0.0-rc.12" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-x64": "1.0.0-rc.12", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" + } + }, "node_modules/safe-regex2": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.0.tgz", @@ -930,6 +2232,46 @@ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", "license": "MIT" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/sonic-boom": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", @@ -939,6 +2281,16 @@ "atomic-sleep": "^1.0.0" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -948,6 +2300,46 @@ "node": ">= 10.x" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/tdigest": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", @@ -966,6 +2358,50 @@ "real-require": "^0.2.0" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/toad-cache": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", @@ -1019,6 +2455,14 @@ } } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -1037,7 +2481,7 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/v8-compile-cache-lib": { @@ -1047,16 +2491,260 @@ "dev": true, "license": "MIT" }, + "node_modules/vite": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz", + "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.12", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.2.tgz", + "integrity": "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.2", + "@vitest/mocker": "4.1.2", + "@vitest/pretty-format": "4.1.2", + "@vitest/runner": "4.1.2", + "@vitest/snapshot": "4.1.2", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.2", + "@vitest/browser-preview": "4.1.2", + "@vitest/browser-webdriverio": "4.1.2", + "@vitest/ui": "4.1.2", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.4" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", @@ -1075,13 +2763,16 @@ "@fastify/postgres": "^6.0.0", "fastify": "^5.0.0", "fastify-plugin": "^5.0.0", + "node-pg-migrate": "^8.0.4", "pino": "^9.0.0", "prom-client": "^15.0.0" }, "devDependencies": { "@types/node": "^22.0.0", + "@types/pg": "^8.20.0", "ts-node": "^10.0.0", - "typescript": "^5.0.0" + "typescript": "^5.0.0", + "vitest": "^4.1.2" } }, "packages/shared": { diff --git a/package.json b/package.json index 1a1cac0..8aa4cb2 100644 --- a/package.json +++ b/package.json @@ -3,5 +3,8 @@ "private": true, "workspaces": [ "packages/*" - ] + ], + "engines": { + "node": ">=24" + } } diff --git a/packages/api/Dockerfile b/packages/api/Dockerfile new file mode 100644 index 0000000..e122a34 --- /dev/null +++ b/packages/api/Dockerfile @@ -0,0 +1,61 @@ +# Stage 1: production 의존성만 설치 +FROM node:24-alpine AS deps + +WORKDIR /app +RUN chown node:node /app +USER node + +COPY --chown=node:node package*.json ./ +COPY --chown=node:node packages/api/package.json ./packages/api/ +COPY --chown=node:node packages/shared/package.json ./packages/shared/ + +# devDependencies 없이 런타임에 필요한 패키지만 포함 +RUN npm ci --omit=dev + +# --- +# Stage 2: 전체 의존성 설치 + 빌드 +# typescript 등 devDependencies가 필요하므로 별도 스테이지에서 빌드 +FROM node:24-alpine AS builder + +WORKDIR /app +RUN chown node:node /app +USER node + +COPY --chown=node:node package*.json ./ +COPY --chown=node:node packages/api/package.json ./packages/api/ +COPY --chown=node:node packages/shared/package.json ./packages/shared/ +RUN npm ci + +COPY --chown=node:node . . + +RUN npm run build -w packages/shared +RUN npm run build -w packages/api + +# --- +# Stage 3: 런타임 이미지 +# node_modules(deps) + dist(builder) +FROM node:24-alpine AS runner + +WORKDIR /app + +# node_modules(심링크 포함) +COPY --from=deps --chown=node:node /app/node_modules ./node_modules + +# package.json -> 심링크 해석 +COPY --from=builder --chown=node:node /app/package.json ./ +COPY --from=builder --chown=node:node /app/packages/api/package.json ./packages/api/ +COPY --from=builder --chown=node:node /app/packages/shared/package.json ./packages/shared/ + +# dist만 +COPY --from=builder --chown=node:node /app/packages/api/dist ./packages/api/dist +COPY --from=builder --chown=node:node /app/packages/shared/dist ./packages/shared/dist + +# 마이그레이션 파일 +COPY --from=builder --chown=node:node /app/packages/api/migrations ./packages/api/migrations + +USER node + +EXPOSE 3000 + +ENTRYPOINT ["node"] +CMD ["packages/api/dist/index.js"] diff --git a/packages/api/Dockerfile.naive b/packages/api/Dockerfile.naive new file mode 100644 index 0000000..8416abe --- /dev/null +++ b/packages/api/Dockerfile.naive @@ -0,0 +1,18 @@ +FROM node:22-alpine + +WORKDIR /app + +COPY package*.json ./ +COPY packages/api/package.json ./packages/api/ +COPY packages/shared/package.json ./packages/shared/ + +RUN npm ci + +COPY . . + +RUN npm run build -w packages/api + +EXPOSE 3000 + +ENTRYPOINT ["node"] +CMD ["packages/api/dist/index.js"] diff --git a/packages/api/Dockerfile.optimized b/packages/api/Dockerfile.optimized new file mode 100644 index 0000000..4d4d4f8 --- /dev/null +++ b/packages/api/Dockerfile.optimized @@ -0,0 +1,30 @@ +FROM node:20-alpine AS builder + +WORKDIR /app +RUN chown node:node /app +USER node + +COPY --chown=node:node package*.json ./ +COPY --chown=node:node packages/api/package.json ./packages/api/ +COPY --chown=node:node packages/shared/package.json ./packages/shared/ +RUN npm ci + +COPY --chown=node:node . . + +RUN npm run build -w packages/api + +FROM node:20-alpine AS runner + +WORKDIR /app + +COPY --from=builder /app/packages/api/dist ./packages/api/dist +COPY --from=builder /app/packages/shared ./packages/shared +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/package.json ./ + +USER node + +EXPOSE 3000 + +ENTRYPOINT ["node"] +CMD ["packages/api/dist/index.js"] diff --git a/packages/api/migrations/001_create_items.js b/packages/api/migrations/001_create_items.js new file mode 100644 index 0000000..234b970 --- /dev/null +++ b/packages/api/migrations/001_create_items.js @@ -0,0 +1,17 @@ +// @ts-nocheck +/** @type {import('node-pg-migrate').MigrationBuilder} */ + +exports.up = (pgm) => { + pgm.createTable('items', { + id: { type: 'serial', primaryKey: true }, + name: { type: 'varchar(255)', notNull: true }, + description: { type: 'text' }, + view_count: { type: 'integer', notNull: true, default: 0 }, + created_at: { type: 'timestamptz', notNull: true, default: pgm.func('NOW()') }, + updated_at: { type: 'timestamptz', notNull: true, default: pgm.func('NOW()') }, + }) +} + +exports.down = (pgm) => { + pgm.dropTable('items') +} diff --git a/packages/api/package.json b/packages/api/package.json index 1026728..8eeadea 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -6,19 +6,25 @@ "scripts": { "build": "tsc", "dev": "ts-node src/index.ts", - "start": "node dist/index.js" + "start": "node dist/index.js", + "migrate": "node-pg-migrate -m migrations up", + "test": "vitest run", + "test:watch": "vitest" }, "dependencies": { "@devopsim/shared": "*", "@fastify/postgres": "^6.0.0", - "fastify-plugin": "^5.0.0", "fastify": "^5.0.0", + "fastify-plugin": "^5.0.0", + "node-pg-migrate": "^8.0.4", "pino": "^9.0.0", "prom-client": "^15.0.0" }, "devDependencies": { "@types/node": "^22.0.0", + "@types/pg": "^8.20.0", "ts-node": "^10.0.0", - "typescript": "^5.0.0" + "typescript": "^5.0.0", + "vitest": "^4.1.2" } } diff --git a/packages/api/src/app.ts b/packages/api/src/app.ts new file mode 100644 index 0000000..cef0a3f --- /dev/null +++ b/packages/api/src/app.ts @@ -0,0 +1,39 @@ +import Fastify, { FastifyBaseLogger } from 'fastify' +import { logger } from '@devopsim/shared' +import { AppError } from './errors' +import dbPlugin from './plugins/db' +import healthRoute from './routes/health' +import itemsRoute from './routes/items' +import { pgItemRepository } from './repositories/items' +import { itemService } from './services/items' + +export function buildApp(opts: { logger?: boolean } = {}) { + // 테스트 시 logger: false, 그 외엔 shared pino logger 주입 + // pino.Logger와 FastifyBaseLogger는 런타임 호환이지만 타입이 다소 달라 단언 필요 + const app = opts.logger === false + ? Fastify({ logger: false }) + : Fastify({ loggerInstance: logger as unknown as FastifyBaseLogger }) + + app.setErrorHandler((error: Error & { statusCode?: number; validation?: unknown }, _req, reply) => { + if (error instanceof AppError) { + reply.code(error.statusCode).send({ message: error.message }) + } else if (error.validation) { + reply.code(400).send({ message: error.message }) + } else { + app.log.error(error) + reply.code(500).send({ message: 'Internal Server Error' }) + } + }) + + app.register(dbPlugin) + + app.after(() => { + const repo = pgItemRepository(app.pg.pool) + const service = itemService(repo) + app.register(itemsRoute, { service }) + }) + + app.register(healthRoute) + + return app +} diff --git a/packages/api/src/domain/item.ts b/packages/api/src/domain/item.ts new file mode 100644 index 0000000..04d200e --- /dev/null +++ b/packages/api/src/domain/item.ts @@ -0,0 +1,26 @@ +export interface Item { + id: number + name: string + description: string | null + view_count: number + created_at: Date + updated_at: Date +} + +export interface CreateItemDto { + name: string + description?: string +} + +export interface UpdateItemDto { + name?: string + description?: string +} + +export interface ItemRepository { + findAll(): Promise + findById(id: number): Promise + create(dto: CreateItemDto): Promise + update(id: number, dto: UpdateItemDto): Promise + remove(id: number): Promise +} diff --git a/packages/api/src/errors.ts b/packages/api/src/errors.ts new file mode 100644 index 0000000..1577792 --- /dev/null +++ b/packages/api/src/errors.ts @@ -0,0 +1,9 @@ +export class AppError extends Error { + constructor( + public readonly statusCode: number, + message: string + ) { + super(message) + this.name = 'AppError' + } +} diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 3fc602f..5b02e55 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -1,11 +1,6 @@ -import Fastify from 'fastify' -import dbPlugin from './plugins/db' -import healthRoute from './routes/health' +import { buildApp } from './app' -const app = Fastify({ logger: true }) - -app.register(dbPlugin) -app.register(healthRoute) +const app = buildApp() const start = async () => { try { diff --git a/packages/api/src/repositories/items.ts b/packages/api/src/repositories/items.ts new file mode 100644 index 0000000..8e4b2bc --- /dev/null +++ b/packages/api/src/repositories/items.ts @@ -0,0 +1,59 @@ +import type { Pool } from 'pg' +import type { Item, ItemRepository } from '../domain/item' + +export function pgItemRepository(db: Pool): ItemRepository { + return { + async findAll() { + const { rows } = await db.query( + 'SELECT * FROM items ORDER BY created_at DESC' + ) + return rows + }, + + async findById(id) { + const { rows } = await db.query( + 'SELECT * FROM items WHERE id = $1', + [id] + ) + return rows[0] ?? null + }, + + async create(dto) { + const { rows } = await db.query( + 'INSERT INTO items (name, description) VALUES ($1, $2) RETURNING *', + [dto.name, dto.description ?? null] + ) + return rows[0] + }, + + async update(id, dto) { + const fields: string[] = [] + const values: unknown[] = [] + let idx = 1 + + if (dto.name !== undefined) { + fields.push(`name = $${idx++}`) + values.push(dto.name) + } + if (dto.description !== undefined) { + fields.push(`description = $${idx++}`) + values.push(dto.description) + } + fields.push(`updated_at = NOW()`) + values.push(id) + const { rows } = await db.query( + `UPDATE items SET ${fields.join(', ')} WHERE id = $${idx} RETURNING *`, + values + ) + return rows[0] ?? null + }, + + async remove(id) { + const { rowCount } = await db.query( + 'DELETE FROM items WHERE id = $1', + [id] + ) + return (rowCount ?? 0) > 0 + }, + } +} diff --git a/packages/api/src/routes/health.ts b/packages/api/src/routes/health.ts index a9ba59b..a04d3f9 100644 --- a/packages/api/src/routes/health.ts +++ b/packages/api/src/routes/health.ts @@ -1,7 +1,8 @@ import { FastifyInstance } from 'fastify' +import type { HealthResponse } from '@devopsim/shared' export default async function healthRoute(app: FastifyInstance) { - app.get('/health', async () => { + app.get<{ Reply: HealthResponse }>('/health', async () => { return { status: 'ok' } }) } diff --git a/packages/api/src/routes/items.ts b/packages/api/src/routes/items.ts new file mode 100644 index 0000000..416a44e --- /dev/null +++ b/packages/api/src/routes/items.ts @@ -0,0 +1,42 @@ +import { FastifyInstance } from 'fastify' +import type { itemService } from '../services/items' +import { createItemSchema, updateItemSchema } from './schemas/items' + +type ItemService = ReturnType + +export default async function itemsRoute( + app: FastifyInstance, + opts: { service: ItemService } +) { + const { service } = opts + + app.get('/items', async () => { + return service.getAll() + }) + + app.get<{ Params: { id: string } }>('/items/:id', async (req) => { + return service.getOne(Number(req.params.id)) + }) + + app.post<{ Body: { name: string; description?: string } }>( + '/items', + { schema: createItemSchema }, + async (req, reply) => { + const item = await service.create(req.body) + reply.code(201).send(item) + } + ) + + app.put<{ Params: { id: string }; Body: { name?: string; description?: string } }>( + '/items/:id', + { schema: updateItemSchema }, + async (req) => { + return service.update(Number(req.params.id), req.body) + } + ) + + app.delete<{ Params: { id: string } }>('/items/:id', async (req, reply) => { + await service.remove(Number(req.params.id)) + reply.code(204).send() + }) +} diff --git a/packages/api/src/routes/schemas/items.ts b/packages/api/src/routes/schemas/items.ts new file mode 100644 index 0000000..854470d --- /dev/null +++ b/packages/api/src/routes/schemas/items.ts @@ -0,0 +1,23 @@ +export const createItemSchema = { + body: { + type: 'object', + required: ['name'], + properties: { + name: { type: 'string', minLength: 1, maxLength: 255 }, + description: { type: 'string' }, + }, + additionalProperties: false, + }, +} as const + +export const updateItemSchema = { + body: { + type: 'object', + properties: { + name: { type: 'string', minLength: 1, maxLength: 255 }, + description: { type: 'string' }, + }, + additionalProperties: false, + minProperties: 1, + }, +} as const diff --git a/packages/api/src/services/items.ts b/packages/api/src/services/items.ts new file mode 100644 index 0000000..69dc2ab --- /dev/null +++ b/packages/api/src/services/items.ts @@ -0,0 +1,34 @@ +import type { ItemRepository, CreateItemDto, UpdateItemDto } from '../domain/item' +import { AppError } from '../errors' + +export function itemService(repo: ItemRepository) { + return { + getAll() { + return repo.findAll() + }, + + async getOne(id: number) { + const item = await repo.findById(id) + if (!item) throw new AppError(404, 'Item not found') + return item + }, + + create(dto: CreateItemDto) { + return repo.create(dto) + }, + + async update(id: number, dto: UpdateItemDto) { + if (dto.name === undefined && dto.description === undefined) { + throw new AppError(400, 'No fields to update') + } + const item = await repo.update(id, dto) + if (!item) throw new AppError(404, 'Item not found') + return item + }, + + async remove(id: number) { + const deleted = await repo.remove(id) + if (!deleted) throw new AppError(404, 'Item not found') + }, + } +} diff --git a/packages/api/src/test/helpers.ts b/packages/api/src/test/helpers.ts new file mode 100644 index 0000000..06b4316 --- /dev/null +++ b/packages/api/src/test/helpers.ts @@ -0,0 +1,8 @@ +import { buildApp } from '../app' +import type { FastifyInstance } from 'fastify' + +export async function createTestApp(): Promise { + const app = buildApp({ logger: false }) + await app.ready() + return app +} diff --git a/packages/api/src/test/items.test.ts b/packages/api/src/test/items.test.ts new file mode 100644 index 0000000..3ceb64c --- /dev/null +++ b/packages/api/src/test/items.test.ts @@ -0,0 +1,263 @@ +import { describe, test, expect, beforeEach, afterEach } from 'vitest' +import type { FastifyInstance } from 'fastify' +import { createTestApp } from './helpers' + +describe('POST /items', () => { + let app: FastifyInstance + + beforeEach(async () => { + app = await createTestApp() + await app.pg.query('TRUNCATE TABLE items RESTART IDENTITY') + }) + + afterEach(async () => { + await app.close() + }) + + // --- ECP: 정상 클래스 --- + + test('정상 요청 → 201 + 생성된 item 반환', async () => { + const res = await app.inject({ + method: 'POST', + url: '/items', + payload: { name: '테스트 아이템', description: '설명' }, + }) + expect(res.statusCode).toBe(201) + const body = res.json() + expect(body.id).toBeDefined() + expect(body.name).toBe('테스트 아이템') + expect(body.description).toBe('설명') + expect(body.view_count).toBe(0) + }) + + test('description 없이 name만 → 201', async () => { + const res = await app.inject({ + method: 'POST', + url: '/items', + payload: { name: '이름만' }, + }) + expect(res.statusCode).toBe(201) + expect(res.json().description).toBeNull() + }) + + // --- ECP: 비정상 클래스 --- + + test('name 누락 → 400', async () => { + const res = await app.inject({ + method: 'POST', + url: '/items', + payload: { description: '이름없음' }, + }) + expect(res.statusCode).toBe(400) + }) + + test('허용되지 않은 필드 포함 → Fastify가 strip 후 201 (additionalProperties 제거)', async () => { + const res = await app.inject({ + method: 'POST', + url: '/items', + payload: { name: '아이템', hack: true }, + }) + // Fastify 기본 동작: 추가 필드를 거절이 아닌 제거(strip)하고 처리 + expect(res.statusCode).toBe(201) + expect(res.json()).not.toHaveProperty('hack') + }) + + // --- BVA: name 경계값 --- + + test('name 0자(빈 문자열) → 400', async () => { + const res = await app.inject({ + method: 'POST', + url: '/items', + payload: { name: '' }, + }) + expect(res.statusCode).toBe(400) + }) + + test('name 1자(최솟값) → 201', async () => { + const res = await app.inject({ + method: 'POST', + url: '/items', + payload: { name: 'a' }, + }) + expect(res.statusCode).toBe(201) + }) + + test('name 255자(최댓값) → 201', async () => { + const res = await app.inject({ + method: 'POST', + url: '/items', + payload: { name: 'a'.repeat(255) }, + }) + expect(res.statusCode).toBe(201) + }) + + test('name 256자(최댓값+1) → 400', async () => { + const res = await app.inject({ + method: 'POST', + url: '/items', + payload: { name: 'a'.repeat(256) }, + }) + expect(res.statusCode).toBe(400) + }) +}) + +describe('GET /items/:id', () => { + let app: FastifyInstance + let createdId: number + + beforeEach(async () => { + app = await createTestApp() + await app.pg.query('TRUNCATE TABLE items RESTART IDENTITY') + const { rows } = await app.pg.query( + `INSERT INTO items (name) VALUES ('조회용 아이템') RETURNING id` + ) + createdId = rows[0].id + }) + + afterEach(async () => { + await app.close() + }) + + // --- ECP --- + + test('존재하는 id → 200 + item 반환', async () => { + const res = await app.inject({ method: 'GET', url: `/items/${createdId}` }) + expect(res.statusCode).toBe(200) + expect(res.json().id).toBe(createdId) + }) + + test('존재하지 않는 id → 404', async () => { + const res = await app.inject({ method: 'GET', url: '/items/999999' }) + expect(res.statusCode).toBe(404) + expect(res.json().message).toBe('Item not found') + }) +}) + +describe('PUT /items/:id', () => { + let app: FastifyInstance + let createdId: number + + beforeEach(async () => { + app = await createTestApp() + await app.pg.query('TRUNCATE TABLE items RESTART IDENTITY') + const { rows } = await app.pg.query( + `INSERT INTO items (name, description) VALUES ('원본', '원본설명') RETURNING id` + ) + createdId = rows[0].id + }) + + afterEach(async () => { + await app.close() + }) + + // --- ECP: 정상 클래스 --- + + test('name만 수정 → 200 + 변경 반영', async () => { + const res = await app.inject({ + method: 'PUT', + url: `/items/${createdId}`, + payload: { name: '수정된 이름' }, + }) + expect(res.statusCode).toBe(200) + expect(res.json().name).toBe('수정된 이름') + expect(res.json().description).toBe('원본설명') + }) + + test('description만 수정 → 200', async () => { + const res = await app.inject({ + method: 'PUT', + url: `/items/${createdId}`, + payload: { description: '수정된 설명' }, + }) + expect(res.statusCode).toBe(200) + expect(res.json().description).toBe('수정된 설명') + }) + + test('name + description 동시 수정 → 200', async () => { + const res = await app.inject({ + method: 'PUT', + url: `/items/${createdId}`, + payload: { name: '새이름', description: '새설명' }, + }) + expect(res.statusCode).toBe(200) + expect(res.json().name).toBe('새이름') + expect(res.json().description).toBe('새설명') + }) + + // --- ECP: 비정상 클래스 --- + + test('빈 body → 400', async () => { + const res = await app.inject({ + method: 'PUT', + url: `/items/${createdId}`, + payload: {}, + }) + expect(res.statusCode).toBe(400) + }) + + test('존재하지 않는 id → 404', async () => { + const res = await app.inject({ + method: 'PUT', + url: '/items/999999', + payload: { name: '수정' }, + }) + expect(res.statusCode).toBe(404) + }) + + // --- BVA: name 경계값 --- + + test('name 1자 수정(최솟값) → 200', async () => { + const res = await app.inject({ + method: 'PUT', + url: `/items/${createdId}`, + payload: { name: 'a' }, + }) + expect(res.statusCode).toBe(200) + }) + + test('name 255자 수정(최댓값) → 200', async () => { + const res = await app.inject({ + method: 'PUT', + url: `/items/${createdId}`, + payload: { name: 'a'.repeat(255) }, + }) + expect(res.statusCode).toBe(200) + }) + + test('name 256자 수정(최댓값+1) → 400', async () => { + const res = await app.inject({ + method: 'PUT', + url: `/items/${createdId}`, + payload: { name: 'a'.repeat(256) }, + }) + expect(res.statusCode).toBe(400) + }) +}) + +describe('DELETE /items/:id', () => { + let app: FastifyInstance + let createdId: number + + beforeEach(async () => { + app = await createTestApp() + await app.pg.query('TRUNCATE TABLE items RESTART IDENTITY') + const { rows } = await app.pg.query( + `INSERT INTO items (name) VALUES ('삭제용') RETURNING id` + ) + createdId = rows[0].id + }) + + afterEach(async () => { + await app.close() + }) + + test('존재하는 id → 204', async () => { + const res = await app.inject({ method: 'DELETE', url: `/items/${createdId}` }) + expect(res.statusCode).toBe(204) + }) + + test('존재하지 않는 id → 404', async () => { + const res = await app.inject({ method: 'DELETE', url: '/items/999999' }) + expect(res.statusCode).toBe(404) + }) +}) diff --git a/packages/api/vitest.config.ts b/packages/api/vitest.config.ts new file mode 100644 index 0000000..8e548db --- /dev/null +++ b/packages/api/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['src/**/*.test.ts'], + env: { + DATABASE_URL: process.env.TEST_DATABASE_URL ?? process.env.DATABASE_URL ?? '', + }, + }, +}) diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts new file mode 100644 index 0000000..5237b70 --- /dev/null +++ b/packages/shared/src/index.ts @@ -0,0 +1,2 @@ +export { logger } from './logger'; +export type { HealthResponse } from './types'; diff --git a/packages/shared/src/logger.ts b/packages/shared/src/logger.ts new file mode 100644 index 0000000..421d5d4 --- /dev/null +++ b/packages/shared/src/logger.ts @@ -0,0 +1,3 @@ +import pino from 'pino'; + +export const logger = pino({ level: 'info' }); diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts new file mode 100644 index 0000000..b033333 --- /dev/null +++ b/packages/shared/src/types.ts @@ -0,0 +1,3 @@ +export interface HealthResponse { + status: 'ok' | 'error'; +} diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json new file mode 100644 index 0000000..75eba9f --- /dev/null +++ b/packages/shared/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src"] +}