commit 891ae276893c038cdd9c49d738035133aa3a470b Author: Your Name Date: Tue Sep 23 01:42:50 2025 +0800 feat: initial commit with accounts manager project structure - TypeScript项目基础架构 - API路由和账户管理服务 - 数据库模式和迁移 - 基础配置文件和文档 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..3d43f2f --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(mkdir:*)", + "Bash(git init:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..efb3396 --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +# Database Configuration +DATABASE_URL=postgresql://username:password@localhost:5432/accounts_db + +# Server Configuration +PORT=3006 +NODE_ENV=development + +# Lock Timeout (in minutes) +LOCK_TIMEOUT_MINUTES=30 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7f4468f --- /dev/null +++ b/.gitignore @@ -0,0 +1,50 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Build outputs +dist/ +build/ +*.tsbuildinfo + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Logs +logs/ +*.log + +# Runtime data +pids/ +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ + +# Database +*.sqlite +*.db \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..204397f --- /dev/null +++ b/README.md @@ -0,0 +1,135 @@ +# Accounts Manager + +轻量化账号管理平台 + +## 技术栈 + +- **运行时**: Node.js (v18+) +- **语言**: TypeScript +- **Web框架**: Fastify +- **数据库**: PostgreSQL (v14+) +- **ORM**: Drizzle ORM +- **数据校验**: Zod + +## 快速开始 + +### 1. 安装依赖 + +```bash +npm install +``` + +### 2. 配置环境变量 + +复制环境变量模板并配置: + +```bash +cp .env.example .env +``` + +编辑 `.env` 文件配置数据库连接: + +```env +DATABASE_URL=postgresql://username:password@localhost:5432/accounts_db +PORT=3000 +NODE_ENV=development +LOCK_TIMEOUT_MINUTES=5 +``` + +### 3. 数据库设置 + +运行数据库迁移: + +```bash +npm run db:generate +npm run db:migrate +``` + +### 4. 启动服务 + +开发模式: +```bash +npm run dev +``` + +生产模式: +```bash +npm run build +npm start +``` + +## API 文档 + +### 脚本专用 API + +**Base URL**: `/s/v1/{ownerId}` + +#### 1. 获取账号 +- **GET** `/s/v1/{ownerId}/acquire?platform={platform}&count={count}` +- 获取可用账号并锁定 + +#### 2. 更新账号状态 +- **GET** `/s/v1/{ownerId}/update/{accountId}/{newStatus}?notes={notes}` +- 更新指定账号状态 + +#### 3. 上传账号 +- **POST** `/s/v1/{ownerId}/upload` +- 批量创建或更新账号 + +### 前端管理 API + +**Base URL**: `/web/v1` + +#### 1. 获取账号列表 +- **POST** `/web/v1/accounts/list` +- 支持复杂查询、分页、排序 + +#### 2. 批量删除账号 +- **POST** `/web/v1/accounts/delete-batch` + +#### 3. 批量更新账号 +- **POST** `/web/v1/accounts/update-batch` + +#### 4. 统计概览 +- **GET** `/web/v1/stats/overview` + +### 健康检查 +- **GET** `/health` + +## 数据库命令 + +```bash +# 生成迁移文件 +npm run db:generate + +# 运行迁移 +npm run db:migrate + +# 打开数据库管理界面 +npm run db:studio +``` + +## 项目结构 + +``` +src/ +├── api/v1/ +│ ├── script/ # 脚本API +│ └── web/ # 前端API +├── core/ # 业务逻辑 +├── db/ # 数据库配置 +├── jobs/ # 后台任务 +├── lib/ # 工具函数 +├── types/ # 类型定义 +├── config.ts # 配置 +└── index.ts # 入口文件 +``` + +## 特性 + +- **单表设计**: 简化数据模型 +- **OwnerID认证**: 基于URL的身份验证 +- **自动锁定管理**: 防止并发冲突 +- **后台清理任务**: 自动释放超时锁定 +- **统一响应格式**: 标准化API响应 +- **类型安全**: 完整的TypeScript支持 \ No newline at end of file diff --git a/docs/API文档.md b/docs/API文档.md new file mode 100644 index 0000000..d6eebb7 --- /dev/null +++ b/docs/API文档.md @@ -0,0 +1,867 @@ +# 账户管理系统 API 文档 + +## 概述 + +本文档描述了账户管理系统的API接口,用于Web前端对接。系统提供了账户的获取、上传、更新、删除和统计等功能。 + +### 基础信息 + +- **基础URL**: `{API_BASE_URL}` +- **数据格式**: JSON +- **字符编码**: UTF-8 +- **认证方式**: 需要在请求头中包含认证信息 + +### 通用响应格式 + +所有API接口都使用统一的响应格式: + +```typescript +interface ApiResponse { + code: BusinessCode; // 业务状态码 + message: string; // 响应消息 + data: T | null; // 响应数据 +} +``` + +#### 业务状态码 (BusinessCode) + +| 状态码 | 名称 | 说明 | +|--------|------|------| +| 0 | Success | 请求成功 | +| 1001 | NoResource | 资源不存在 | +| 2001 | InvalidParams | 参数无效 | +| 3001 | ResourceConflict | 资源冲突 | +| 4001 | PermissionDenied | 权限不足 | +| 5001 | BusinessError | 业务错误 | + +## 数据模型 + +### 账户 (Account) + +账户是系统的核心数据模型,包含以下字段: + +```typescript +interface Account { + id: number; // 账户ID,自增主键 + ownerId: string; // 所有者ID + platform: string; // 平台名称 + customId: string; // 自定义ID + data: string; // 账户数据,JSON格式字符串 + status: string; // 账户状态 + notes?: string; // 备注,可选 + lockedAt?: Date; // 锁定时间,可选 + createdAt: Date; // 创建时间 + updatedAt: Date; // 更新时间 +} +``` + +#### 账户状态 + +- `available`: 可用 +- `locked`: 已锁定 +- 其他自定义状态 + +### 分页结果 (PaginationResult) + +```typescript +interface PaginationResult { + page: number; // 当前页码 + pageSize: number; // 每页条数 + total: number; // 总记录数 + totalPages: number; // 总页数 +} +``` + +### 统计概览 (StatsOverview) + +```typescript +interface StatsOverview { + totalAccounts: number; // 总账户数 + platformSummary: Record; // 平台统计 + ownerSummary: Record; // 所有者统计 + statusSummary: Record; // 状态统计 + detailedBreakdown: { // 详细统计 + platform: string; + ownerId: string; + status: string; + count: number; + }[]; +} +``` + +## API 接口 + +### 1. 账户获取接口 + +用于脚本端获取可用账户。 + +#### 请求 + +```http +GET /s/v1/{ownerId}/acquire?platform={platform}&count={count} +``` + +#### 参数 + +| 参数名 | 位置 | 类型 | 必填 | 说明 | +|--------|------|------|------|------| +| ownerId | 路径 | string | 是 | 所有者ID | +| platform | 查询 | string | 是 | 平台名称 | +| count | 查询 | number | 否 | 获取数量,默认1,最大100 | + +#### 响应 + +成功响应: + +```json +{ + "code": 0, + "message": "Successfully acquired 1 account.", + "data": [ + { + "id": 1, + "customId": "custom123", + "data": "{\"username\":\"test\",\"password\":\"123456\"}" + } + ] +} +``` + +无可用账户响应: + +```json +{ + "code": 1001, + "message": "No available accounts found for platform 'example'.", + "data": null +} +``` + +#### 示例 + +```javascript +// 使用fetch获取账户 +async function acquireAccounts(ownerId, platform, count = 1) { + const response = await fetch(`/s/v1/${ownerId}/acquire?platform=${platform}&count=${count}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer YOUR_TOKEN' + } + }); + + const result = await response.json(); + return result; +} + +// 调用示例 +const result = await acquireAccounts('owner123', 'example', 2); +if (result.code === 0) { + console.log('获取成功:', result.data); +} else { + console.error('获取失败:', result.message); +} +``` + +### 2. 账户状态更新接口 + +用于脚本端更新账户状态。 + +#### 请求 + +```http +GET /s/v1/{ownerId}/update/{accountId}/{newStatus}?notes={notes} +``` + +#### 参数 + +| 参数名 | 位置 | 类型 | 必填 | 说明 | +|--------|------|------|------|------| +| ownerId | 路径 | string | 是 | 所有者ID | +| accountId | 路径 | number | 是 | 账户ID | +| newStatus | 路径 | string | 是 | 新状态 | +| notes | 查询 | string | 否 | 备注 | + +#### 响应 + +成功响应: + +```json +{ + "code": 0, + "message": "Account status updated to 'used'.", + "data": { + "updatedId": 1 + } +} +``` + +权限不足响应: + +```json +{ + "code": 4001, + "message": "Account not found or permission denied.", + "data": null +} +``` + +#### 示例 + +```javascript +// 使用fetch更新账户状态 +async function updateAccountStatus(ownerId, accountId, newStatus, notes) { + const url = `/s/v1/${ownerId}/update/${accountId}/${newStatus}`; + if (notes) { + url += `?notes=${encodeURIComponent(notes)}`; + } + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer YOUR_TOKEN' + } + }); + + const result = await response.json(); + return result; +} + +// 调用示例 +const result = await updateAccountStatus('owner123', 1, 'used', '测试使用'); +if (result.code === 0) { + console.log('更新成功:', result.data); +} else { + console.error('更新失败:', result.message); +} +``` + +### 3. 账户上传接口 + +用于脚本端批量上传账户。 + +#### 请求 + +```http +POST /s/v1/{ownerId}/upload +``` + +#### 请求体 + +```json +[ + { + "platform": "string", + "customId": "string", + "data": "string", + "status": "string" + } +] +``` + +#### 参数 + +| 参数名 | 位置 | 类型 | 必填 | 说明 | +|--------|------|------|------|------| +| ownerId | 路径 | string | 是 | 所有者ID | +| body | 请求体 | ScriptUploadItem[] | 是 | 账户数据数组 | + +#### ScriptUploadItem 类型 + +```typescript +interface ScriptUploadItem { + platform: string; // 平台名称 + customId: string; // 自定义ID + data: string; // 账户数据,JSON格式字符串 + status?: string; // 账户状态,可选,默认为"available" +} +``` + +#### 响应 + +成功响应: + +```json +{ + "code": 0, + "message": "Successfully processed 2 accounts (1 created, 1 updated).", + "data": { + "processedCount": 2, + "createdCount": 1, + "updatedCount": 1 + } +} +``` + +#### 示例 + +```javascript +// 使用fetch上传账户 +async function uploadAccounts(ownerId, accounts) { + const response = await fetch(`/s/v1/${ownerId}/upload`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer YOUR_TOKEN' + }, + body: JSON.stringify(accounts) + }); + + const result = await response.json(); + return result; +} + +// 调用示例 +const accounts = [ + { + platform: 'example', + customId: 'user123', + data: JSON.stringify({ username: 'user123', password: 'pass123' }), + status: 'available' + }, + { + platform: 'example', + customId: 'user456', + data: JSON.stringify({ username: 'user456', password: 'pass456' }) + } +]; + +const result = await uploadAccounts('owner123', accounts); +if (result.code === 0) { + console.log('上传成功:', result.data); +} else { + console.error('上传失败:', result.message); +} +``` + +### 4. 账户列表接口 + +用于Web端获取账户列表,支持筛选、分页和排序。 + +#### 请求 + +```http +POST /web/v1/accounts/list +``` + +#### 请求体 + +```json +{ + "filters": { + "platform": "string", + "status": ["string"], + "ownerId": "string", + "search": "string" + }, + "pagination": { + "page": 1, + "pageSize": 10 + }, + "sort": { + "field": "id", + "order": "desc" + } +} +``` + +#### 参数 + +| 参数名 | 位置 | 类型 | 必填 | 说明 | +|--------|------|------|------|------| +| filters | 请求体 | ListAccountsFilters | 否 | 筛选条件 | +| pagination | 请求体 | object | 是 | 分页参数 | +| sort | 请求体 | object | 是 | 排序参数 | + +#### ListAccountsFilters 类型 + +```typescript +interface ListAccountsFilters { + platform?: string; // 平台名称筛选 + status?: string[]; // 状态筛选,多选 + ownerId?: string; // 所有者ID筛选 + search?: string; // 搜索关键词,搜索customId和notes +} +``` + +#### 排序字段 + +支持以下字段排序: +- `id`: 账户ID +- `ownerId`: 所有者ID +- `platform`: 平台名称 +- `customId`: 自定义ID +- `data`: 账户数据 +- `status`: 账户状态 +- `notes`: 备注 +- `lockedAt`: 锁定时间 +- `createdAt`: 创建时间 +- `updatedAt`: 更新时间 + +#### 响应 + +成功响应: + +```json +{ + "code": 0, + "message": "Success", + "data": { + "list": [ + { + "id": 1, + "ownerId": "owner123", + "platform": "example", + "customId": "user123", + "data": "{\"username\":\"user123\",\"password\":\"pass123\"}", + "status": "available", + "notes": "测试账户", + "lockedAt": null, + "createdAt": "2023-01-01T00:00:00.000Z", + "updatedAt": "2023-01-01T00:00:00.000Z" + } + ], + "pagination": { + "page": 1, + "pageSize": 10, + "total": 1, + "totalPages": 1 + } + } +} +``` + +#### 示例 + +```javascript +// 使用fetch获取账户列表 +async function getAccountsList(filters, pagination, sort) { + const response = await fetch('/web/v1/accounts/list', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer YOUR_TOKEN' + }, + body: JSON.stringify({ + filters, + pagination, + sort + }) + }); + + const result = await response.json(); + return result; +} + +// 调用示例 +const filters = { + platform: 'example', + status: ['available'], + search: 'test' +}; + +const pagination = { + page: 1, + pageSize: 10 +}; + +const sort = { + field: 'createdAt', + order: 'desc' +}; + +const result = await getAccountsList(filters, pagination, sort); +if (result.code === 0) { + console.log('账户列表:', result.data.list); + console.log('分页信息:', result.data.pagination); +} else { + console.error('获取失败:', result.message); +} +``` + +### 5. 批量删除账户接口 + +用于Web端批量删除账户。 + +#### 请求 + +```http +POST /web/v1/accounts/delete-batch +``` + +#### 请求体 + +```json +{ + "ids": [1, 2, 3] +} +``` + +#### 参数 + +| 参数名 | 位置 | 类型 | 必填 | 说明 | +|--------|------|------|------|------| +| ids | 请求体 | number[] | 是 | 要删除的账户ID数组 | + +#### 响应 + +成功响应: + +```json +{ + "code": 0, + "message": "Successfully deleted 3 accounts.", + "data": { + "deletedCount": 3 + } +} +``` + +#### 示例 + +```javascript +// 使用fetch批量删除账户 +async function batchDeleteAccounts(ids) { + const response = await fetch('/web/v1/accounts/delete-batch', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer YOUR_TOKEN' + }, + body: JSON.stringify({ ids }) + }); + + const result = await response.json(); + return result; +} + +// 调用示例 +const result = await batchDeleteAccounts([1, 2, 3]); +if (result.code === 0) { + console.log('删除成功:', result.data); +} else { + console.error('删除失败:', result.message); +} +``` + +### 6. 批量更新账户接口 + +用于Web端批量更新账户。 + +#### 请求 + +```http +POST /web/v1/accounts/update-batch +``` + +#### 请求体 + +```json +{ + "ids": [1, 2, 3], + "payload": { + "status": "available", + "ownerId": "newOwner", + "notes": "批量更新备注" + } +} +``` + +#### 参数 + +| 参数名 | 位置 | 类型 | 必填 | 说明 | +|--------|------|------|------|------| +| ids | 请求体 | number[] | 是 | 要更新的账户ID数组 | +| payload | 请求体 | object | 是 | 更新数据 | + +#### payload 类型 + +```typescript +interface UpdatePayload { + status?: string; // 新状态 + ownerId?: string; // 新所有者ID + notes?: string; // 新备注 +} +``` + +#### 响应 + +成功响应: + +```json +{ + "code": 0, + "message": "Successfully updated 3 accounts.", + "data": { + "updatedCount": 3 + } +} +``` + +#### 示例 + +```javascript +// 使用fetch批量更新账户 +async function batchUpdateAccounts(ids, payload) { + const response = await fetch('/web/v1/accounts/update-batch', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer YOUR_TOKEN' + }, + body: JSON.stringify({ ids, payload }) + }); + + const result = await response.json(); + return result; +} + +// 调用示例 +const ids = [1, 2, 3]; +const payload = { + status: 'available', + notes: '批量更新为可用状态' +}; + +const result = await batchUpdateAccounts(ids, payload); +if (result.code === 0) { + console.log('更新成功:', result.data); +} else { + console.error('更新失败:', result.message); +} +``` + +### 7. 统计概览接口 + +用于Web端获取账户统计信息。 + +#### 请求 + +```http +GET /web/v1/stats/overview +``` + +#### 响应 + +成功响应: + +```json +{ + "code": 0, + "message": "Success", + "data": { + "totalAccounts": 100, + "platformSummary": { + "example": 50, + "test": 30, + "demo": 20 + }, + "ownerSummary": { + "owner1": 40, + "owner2": 35, + "owner3": 25 + }, + "statusSummary": { + "available": 70, + "locked": 20, + "used": 10 + }, + "detailedBreakdown": [ + { + "platform": "example", + "ownerId": "owner1", + "status": "available", + "count": 20 + }, + { + "platform": "example", + "ownerId": "owner1", + "status": "locked", + "count": 10 + } + ] + } +} +``` + +#### 示例 + +```javascript +// 使用fetch获取统计概览 +async function getStatsOverview() { + const response = await fetch('/web/v1/stats/overview', { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer YOUR_TOKEN' + } + }); + + const result = await response.json(); + return result; +} + +// 调用示例 +const result = await getStatsOverview(); +if (result.code === 0) { + console.log('总账户数:', result.data.totalAccounts); + console.log('平台统计:', result.data.platformSummary); + console.log('所有者统计:', result.data.ownerSummary); + console.log('状态统计:', result.data.statusSummary); + console.log('详细统计:', result.data.detailedBreakdown); +} else { + console.error('获取失败:', result.message); +} +``` + +## 错误处理 + +所有API接口在发生错误时都会返回统一的错误响应格式: + +```json +{ + "code": "错误码", + "message": "错误信息", + "data": null +} +``` + +### 常见错误码 + +| 错误码 | 说明 | 处理建议 | +|--------|------|----------| +| 1001 | 资源不存在 | 检查请求的资源是否存在 | +| 2001 | 参数无效 | 检查请求参数是否符合要求 | +| 3001 | 资源冲突 | 检查是否有重复数据 | +| 4001 | 权限不足 | 检查认证信息是否正确 | +| 5001 | 业务错误 | 检查业务逻辑是否正确 | + +## 最佳实践 + +1. **认证**:所有API请求都需要在请求头中包含认证信息,例如: + ```http + Authorization: Bearer YOUR_TOKEN + ``` + +2. **错误处理**:前端应该根据返回的`code`值进行相应的错误处理,而不是依赖`message`字段。 + +3. **分页**:在获取列表数据时,合理设置分页大小,建议不超过100条。 + +4. **批量操作**:批量操作时,建议限制单次操作的数量,避免服务器压力过大。 + +5. **数据格式**:账户数据字段`data`存储的是JSON格式字符串,前端需要进行相应的序列化和反序列化操作。 + +## 类型定义 + +以下是完整的TypeScript类型定义,可以直接在前端项目中使用: + +```typescript +// 业务状态码 +enum BusinessCode { + Success = 0, + NoResource = 1001, + InvalidParams = 2001, + ResourceConflict = 3001, + PermissionDenied = 4001, + BusinessError = 5001, +} + +// 通用响应格式 +interface ApiResponse { + code: BusinessCode; + message: string; + data: T | null; +} + +// 账户类型 +interface Account { + id: number; + ownerId: string; + platform: string; + customId: string; + data: string; + status: string; + notes?: string; + lockedAt?: Date | null; + createdAt: Date; + updatedAt: Date; +} + +// 分页结果 +interface PaginationResult { + page: number; + pageSize: number; + total: number; + totalPages: number; +} + +// 脚本获取响应 +type ScriptAcquireResponse = Pick[]; + +// 脚本上传项 +interface ScriptUploadItem { + platform: string; + customId: string; + data: string; + status?: string; +} + +// 账户列表筛选条件 +interface ListAccountsFilters { + platform?: string; + status?: string[]; + ownerId?: string; + search?: string; +} + +// 账户列表请求体 +interface ListAccountsBody { + filters: ListAccountsFilters; + pagination: { + page: number; + pageSize: number; + }; + sort: { + field: keyof Account; + order: 'asc' | 'desc'; + }; +} + +// 账户列表响应 +interface ListAccountsResponse { + list: Account[]; + pagination: PaginationResult; +} + +// 批量删除请求体 +interface BatchDeleteBody { + ids: number[]; +} + +// 批量更新请求体 +interface BatchUpdateBody { + ids: number[]; + payload: Partial>; +} + +// 统计概览 +interface StatsOverview { + totalAccounts: number; + platformSummary: Record; + ownerSummary: Record; + statusSummary: Record; + detailedBreakdown: { + platform: string; + ownerId: string; + status: string; + count: number; + }[]; +} +``` + +## 版本历史 + +| 版本 | 日期 | 说明 | +|------|------|------| +| 1.0.0 | 2023-01-01 | 初始版本 | \ No newline at end of file diff --git a/docs/设计.md b/docs/设计.md new file mode 100644 index 0000000..35b8e11 --- /dev/null +++ b/docs/设计.md @@ -0,0 +1,328 @@ +### **轻量化账号管理平台 - 工程设计蓝图** + +#### 1. 核心设计哲学 + +* **单表驱动**: 所有核心数据聚合在 `accounts` 表,简化数据库模型。 +* **OwnerID 即身份**: `ownerId` 同时作为拥有者标识和API认证凭证。 +* **配置硬编码**: 关键状态(如 `available`, `locked`)硬编码在业务逻辑中,降低系统复杂性。 +* **职责分离的API**: 为自动化脚本和管理后台提供两套独立的、高度优化的API。 +* **规范化通信**: 所有API遵循统一的响应结构,业务结果通过 `code` 字段传递。 +* **高性能与健壮性**: 依赖PostgreSQL原生特性(事务、行锁、索引)和专用的后台任务确保系统稳定。 + +#### 2. 技术栈 + +* **运行时**: Node.js (v18+) +* **语言**: TypeScript +* **Web 框架**: **Fastify** +* **数据库**: PostgreSQL (v14+) +* **ORM/查询构建器**: **Drizzle ORM** +* **数据校验**: **Zod** + +#### 3. 项目结构 + +采用模块化、可扩展的目录结构,清晰分离各项职责。 + +``` +. +├── drizzle/ # Drizzle ORM 迁移文件 +├── src/ +│ ├── api/ # API 路由定义 +│ │ └── v1/ +│ │ ├── web/ # 前端管理后台 API +│ │ │ ├── accounts.ts # 账号增删改查 +│ │ │ └── stats.ts # 统计数据 +│ │ └── script/ # 自动化脚本 API +│ │ └── actions.ts # 获取、更新、上传 +│ ├── core/ # 核心业务逻辑 (Services) +│ │ └── AccountService.ts # 封装所有与账号相关的业务操作 +│ ├── db/ # 数据库配置与 Schema +│ │ ├── index.ts # Drizzle 客户端实例 +│ │ └── schema.ts # 数据库表定义 +│ ├── jobs/ # 后台定时任务 +│ │ └── staleLockCleanup.ts # 清理超时锁定的任务 +│ ├── lib/ # 通用库、工具函数 +│ │ └── apiResponse.ts # 统一响应格式的辅助函数 +│ ├── types/ # 全局类型定义 +│ │ ├── api.ts # API 请求/响应体类型 +│ │ └── index.ts # 核心业务模型类型 +│ ├── index.ts # 应用入口,启动 Fastify 服务器 +│ └── config.ts # 应用配置 (数据库连接、端口等) +├── .env # 环境变量 +├── .env.example +├── package.json +└── tsconfig.json +``` + +#### 4. 数据库设计 (单表模型) + +```typescript +// file: src/db/schema.ts +import { pgTable, serial, varchar, timestamp, uniqueIndex, text, index } from 'drizzle-orm/pg-core'; + +export const accounts = pgTable('accounts', { + id: serial('id').primaryKey(), + ownerId: varchar('owner_id', { length: 128 }).notNull(), + platform: varchar('platform', { length: 100 }).notNull(), + customId: varchar('custom_id', { length: 255 }).notNull(), + data: text('data').notNull(), + status: varchar('status', { length: 50 }).notNull(), + notes: text('notes'), + lockedAt: timestamp('locked_at'), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().notNull(), +}, (table) => ({ + platformCustomIdIdx: uniqueIndex('platform_custom_id_idx').on(table.platform, table.customId), + ownerIdStatusIdx: index('owner_id_status_idx').on(table.ownerId, table.status), + platformOwnerIdx: index('platform_owner_idx').on(table.platform, table.ownerId), +})); +``` + +#### 5. 核心类型与接口定义 + +##### 5.1. 业务模型 + +```typescript +// file: src/types/index.ts +import { accounts } from '../db/schema'; +import { InferSelectModel } from 'drizzle-orm'; + +// 从数据库 Schema 自动推断出的 Account 类型,确保与数据库一致 +export type Account = InferSelectModel; +``` + +##### 5.2. API 通信规范 + +```typescript +// file: src/types/api.ts + +/** + * 业务状态码枚举,避免使用魔法数字 + */ +export enum BusinessCode { + Success = 0, + NoResource = 1001, + InvalidParams = 2001, + ResourceConflict = 3001, + PermissionDenied = 4001, // 业务层权限拒绝 + BusinessError = 5001, +} + +/** + * 统一的 API 响应体结构 + */ +export interface ApiResponse { + code: BusinessCode; + message: string; + data: T | null; +} + +/** + * 分页查询结果 + */ +export interface PaginationResult { + page: number; + pageSize: number; + total: number; + totalPages: number; +} +``` + +##### 5.3. API 请求/响应体类型 + +```typescript +// file: src/types/api.ts + +// --- 脚本 API --- + +export type ScriptAcquireResponse = Pick[]; + +export interface ScriptUploadItem { + platform: string; + customId: string; + data: string; + status?: string; +} + +// --- 前端管理 API --- + +export interface ListAccountsFilters { + platform?: string; + status?: string[]; + ownerId?: string; + search?: string; // 模糊搜索 customId 或 notes +} + +export interface ListAccountsBody { + filters: ListAccountsFilters; + pagination: { + page: number; + pageSize: number; + }; + sort: { + field: keyof Account; + order: 'asc' | 'desc'; + }; +} + +export interface ListAccountsResponse { + list: Account[]; + pagination: PaginationResult; +} + +export interface BatchDeleteBody { + ids: number[]; +} + +export interface BatchUpdateBody { + ids: number[]; + payload: Partial>; +} + +export interface StatsOverview { + totalAccounts: number; + platformSummary: Record; + ownerSummary: Record; + statusSummary: Record; + detailedBreakdown: { + platform: string; + ownerId: string; + status: string; + count: number; + }[]; +} +``` + +--- + +### **6. API 设计** + +#### A. 脚本专用 API (Base URL: `/s/v1/{ownerId}`) + +**认证**: 所有请求通过 URL 中的 `{ownerId}` 进行身份验证和授权。 + +1. **获取账号** + * **Endpoint**: `GET /acquire` + * **描述**: 获取一个或多个可用的账号,并将其状态锁定。 + * **Query Params**: + * `platform` (string, **必须**): 平台标识。 + * `count` (number, 可选, 默认 1): 获取数量。 + * **成功响应 (200 OK)**: + ```json + // Body: ApiResponse + { + "code": 0, // BusinessCode.Success + "message": "Successfully acquired 2 accounts.", + "data": [ + { "id": 101, "customId": "user1@gmail.com", "data": "cookie_string_1" }, + { "id": 102, "customId": "user2@gmail.com", "data": "cookie_string_2" } + ] + } + ``` + * **业务失败响应 (200 OK)**: + ```json + // Body: ApiResponse + { + "code": 1001, // BusinessCode.NoResource + "message": "No available accounts found for platform 'google'.", + "data": null + } + ``` + +2. **更新账号状态** + * **Endpoint**: `GET /update/{accountId}/{newStatus}` + * **描述**: 快速更新指定账号的状态。 + * **Query Params**: + * `notes` (string, 可选): 添加备注信息。 + * **成功响应 (200 OK)**: + ```json + // Body: ApiResponse<{ updatedId: number }> + { + "code": 0, + "message": "Account status updated to 'banned'.", + "data": { "updatedId": 12345 } + } + ``` + * **传输错误响应 (403 Forbidden)**: 当 URL 中的 `ownerId` 与 `accountId` 对应的账号所有者不匹配时返回。 + +3. **上传/更新账号** + * **Endpoint**: `POST /upload` + * **描述**: 批量创建或更新账号。 + * **Request Body**: `ScriptUploadItem[]` + * **成功响应 (200 OK)**: + ```json + // Body: ApiResponse<{ processedCount: number, createdCount: number, updatedCount: number }> + { + "code": 0, + "message": "Successfully processed 10 accounts (5 created, 5 updated).", + "data": { "processedCount": 10, "createdCount": 5, "updatedCount": 5 } + } + ``` + +#### B. 前端管理 API (Base URL: `/web/v1`) + +1. **获取账号列表 (复杂查询)** + * **Endpoint**: `POST /accounts/list` + * **描述**: 支持多维度筛选、分页和排序的账号查询。 + * **Request Body**: `ListAccountsBody` + * **成功响应 (200 OK)**: + ```json + // Body: ApiResponse + { + "code": 0, + "message": "Success", + "data": { + "list": [ /* ... Account 对象数组 ... */ ], + "pagination": { "page": 1, "pageSize": 50, "total": 123, "totalPages": 3 } + } + } + ``` + +2. **批量删除账号** + * **Endpoint**: `POST /accounts/delete-batch` + * **描述**: 根据 ID 列表批量删除账号。 + * **Request Body**: `BatchDeleteBody` + * **成功响应 (200 OK)**: + ```json + // Body: ApiResponse<{ deletedCount: number }> + { + "code": 0, + "message": "Successfully deleted 5 accounts.", + "data": { "deletedCount": 5 } + } + ``` + +3. **批量更新账号** + * **Endpoint**: `POST /accounts/update-batch` + * **描述**: 批量修改账号的状态、所有者或备注。 + * **Request Body**: `BatchUpdateBody` + * **成功响应 (200 OK)**: + ```json + // Body: ApiResponse<{ updatedCount: number }> + { + "code": 0, + "message": "Successfully updated 3 accounts.", + "data": { "updatedCount": 3 } + } + ``` + +4. **核心统计接口** + * **Endpoint**: `GET /stats/overview` + * **描述**: 一次性获取仪表盘所需的全部聚合统计数据。 + * **成功响应 (200 OK)**: + ```json + // Body: ApiResponse + { + "code": 0, + "message": "Success", + "data": { /* ... StatsOverview 对象 ... */ } + } + ``` + +### **7. 关键后台任务** + +* **任务名称**: Stale Lock Cleanup (超时锁定清理) +* **执行频率**: 建议每 1 分钟执行一次。 +* **核心逻辑**: + * 查找所有 `status` 为 `locked` 且 `lockedAt` 时间早于预设阈值(例如 5 分钟前)的账号。 + * 执行 SQL: `UPDATE accounts SET status = 'available', lockedAt = NULL WHERE status = 'locked' AND lockedAt < NOW() - INTERVAL '5 minutes';` +* **目的**: 自动释放因脚本异常中断而未能解锁的账号,保证账号资源的流转性。 \ No newline at end of file diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..4593357 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'drizzle-kit'; + +export default defineConfig({ + schema: './src/db/schema.ts', + out: './drizzle', + dialect: 'postgresql', + dbCredentials: { + url: process.env.DATABASE_URL!, + }, +}); \ No newline at end of file diff --git a/drizzle/0000_initial_schema.sql b/drizzle/0000_initial_schema.sql new file mode 100644 index 0000000..7e66991 --- /dev/null +++ b/drizzle/0000_initial_schema.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS "accounts" ( + "id" serial PRIMARY KEY NOT NULL, + "owner_id" varchar(128) NOT NULL, + "platform" varchar(100) NOT NULL, + "custom_id" varchar(255) NOT NULL, + "data" text NOT NULL, + "status" varchar(50) NOT NULL, + "notes" text, + "locked_at" timestamp, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); + +CREATE UNIQUE INDEX IF NOT EXISTS "platform_custom_id_idx" ON "accounts" ("platform","custom_id"); +CREATE INDEX IF NOT EXISTS "owner_id_status_idx" ON "accounts" ("owner_id","status"); +CREATE INDEX IF NOT EXISTS "platform_owner_idx" ON "accounts" ("platform","owner_id"); \ No newline at end of file diff --git a/drizzle/0000_wet_joshua_kane.sql b/drizzle/0000_wet_joshua_kane.sql new file mode 100644 index 0000000..5afb02d --- /dev/null +++ b/drizzle/0000_wet_joshua_kane.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS "accounts" ( + "id" serial PRIMARY KEY NOT NULL, + "owner_id" varchar(128) NOT NULL, + "platform" varchar(100) NOT NULL, + "custom_id" varchar(255) NOT NULL, + "data" text NOT NULL, + "status" varchar(50) NOT NULL, + "notes" text, + "locked_at" timestamp, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "platform_custom_id_idx" ON "accounts" USING btree ("platform","custom_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "owner_id_status_idx" ON "accounts" USING btree ("owner_id","status");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "platform_owner_idx" ON "accounts" USING btree ("platform","owner_id"); \ No newline at end of file diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..b4f719e --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -0,0 +1,152 @@ +{ + "id": "57f2c5ed-21e7-4918-8534-8cca691243fb", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "custom_id": { + "name": "custom_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "platform_custom_id_idx": { + "name": "platform_custom_id_idx", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "custom_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "owner_id_status_idx": { + "name": "owner_id_status_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "platform_owner_idx": { + "name": "platform_owner_idx", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json new file mode 100644 index 0000000..bfc5241 --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1758524929427, + "tag": "0000_wet_joshua_kane", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..eab3b8c --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2242 @@ +{ + "name": "accounts-manager", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "accounts-manager", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@fastify/cors": "^9.0.1", + "dotenv": "^17.2.2", + "drizzle-orm": "^0.33.0", + "fastify": "^4.28.1", + "postgres": "^3.4.4", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/node": "^20.14.9", + "drizzle-kit": "^0.24.0", + "tsx": "^4.16.0", + "typescript": "^5.5.2" + } + }, + "node_modules/@drizzle-team/brocli": { + "version": "0.10.2", + "resolved": "https://registry.npmmirror.com/@drizzle-team/brocli/-/brocli-0.10.2.tgz", + "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@esbuild-kit/core-utils": { + "version": "3.3.2", + "resolved": "https://registry.npmmirror.com/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", + "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==", + "deprecated": "Merged into tsx: https://tsx.is", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.18.20", + "source-map-support": "^0.5.21" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/@esbuild-kit/esm-loader": { + "version": "2.6.5", + "resolved": "https://registry.npmmirror.com/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz", + "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==", + "deprecated": "Merged into tsx: https://tsx.is", + "dev": true, + "license": "MIT", + "dependencies": { + "@esbuild-kit/core-utils": "^3.3.2", + "get-tsconfig": "^4.7.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", + "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", + "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", + "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@fastify/ajv-compiler": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/@fastify/ajv-compiler/-/ajv-compiler-3.6.0.tgz", + "integrity": "sha512-LwdXQJjmMD+GwLOkP7TVC68qa+pSSogeWWmznRJ/coyTcfe9qA05AHFSe1eZFwK6q+xVRpChnvFUkf1iYaSZsQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.11.0", + "ajv-formats": "^2.1.1", + "fast-uri": "^2.0.0" + } + }, + "node_modules/@fastify/cors": { + "version": "9.0.1", + "resolved": "https://registry.npmmirror.com/@fastify/cors/-/cors-9.0.1.tgz", + "integrity": "sha512-YY9Ho3ovI+QHIL2hW+9X4XqQjXLjJqsU+sMV/xFsxZkE8p3GNnYVFpoOxF7SsP5ZL76gwvbo3V9L+FIekBGU4Q==", + "license": "MIT", + "dependencies": { + "fastify-plugin": "^4.0.0", + "mnemonist": "0.39.6" + } + }, + "node_modules/@fastify/error": { + "version": "3.4.1", + "resolved": "https://registry.npmmirror.com/@fastify/error/-/error-3.4.1.tgz", + "integrity": "sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==", + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-4.3.0.tgz", + "integrity": "sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==", + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^5.7.0" + } + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.1.1", + "resolved": "https://registry.npmmirror.com/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz", + "integrity": "sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, + "node_modules/@types/node": { + "version": "20.19.17", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-20.19.17.tgz", + "integrity": "sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "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": "2.1.1", + "resolved": "https://registry.npmmirror.com/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv/node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "8.4.0", + "resolved": "https://registry.npmmirror.com/avvio/-/avvio-8.4.0.tgz", + "integrity": "sha512-CDSwaxINFy59iNwhYnkvALBwZiTydGkOecZyPkqBpABYR1KqGEsET0VOOYDwtleZSUIdeY36DC2bSZ24CO1igA==", + "license": "MIT", + "dependencies": { + "@fastify/error": "^3.3.0", + "fastq": "^1.17.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dotenv": { + "version": "17.2.2", + "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-17.2.2.tgz", + "integrity": "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/drizzle-kit": { + "version": "0.24.2", + "resolved": "https://registry.npmmirror.com/drizzle-kit/-/drizzle-kit-0.24.2.tgz", + "integrity": "sha512-nXOaTSFiuIaTMhS8WJC2d4EBeIcN9OSt2A2cyFbQYBAZbi7lRsVGJNqDpEwPqYfJz38yxbY/UtbvBBahBfnExQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@drizzle-team/brocli": "^0.10.1", + "@esbuild-kit/esm-loader": "^2.5.5", + "esbuild": "^0.19.7", + "esbuild-register": "^3.5.0" + }, + "bin": { + "drizzle-kit": "bin.cjs" + } + }, + "node_modules/drizzle-orm": { + "version": "0.33.0", + "resolved": "https://registry.npmmirror.com/drizzle-orm/-/drizzle-orm-0.33.0.tgz", + "integrity": "sha512-SHy72R2Rdkz0LEq0PSG/IdvnT3nGiWuRk+2tXZQ90GVq/XQhpCzu/EFT3V2rox+w8MlkBQxifF8pCStNYnERfA==", + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=3", + "@electric-sql/pglite": ">=0.1.1", + "@libsql/client": "*", + "@neondatabase/serverless": ">=0.1", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/react": ">=18", + "@types/sql.js": "*", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=13.2.0", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "react": ">=18", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/react": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "react": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, + "node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/esbuild-register": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/esbuild-register/-/esbuild-register-3.6.0.tgz", + "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "peerDependencies": { + "esbuild": ">=0.12 <1" + } + }, + "node_modules/fast-content-type-parse": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz", + "integrity": "sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==", + "license": "MIT" + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stringify": { + "version": "5.16.1", + "resolved": "https://registry.npmmirror.com/fast-json-stringify/-/fast-json-stringify-5.16.1.tgz", + "integrity": "sha512-KAdnLvy1yu/XrRtP+LJnxbBGrhN+xXu+gt3EUvZhYGKCr3lFHq/7UFJHHFgmJKoqlh6B40bZLEv7w46B0mqn1g==", + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.1.0", + "ajv": "^8.10.0", + "ajv-formats": "^3.0.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^2.1.0", + "json-schema-ref-resolver": "^1.0.1", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-json-stringify/node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/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/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-redact": { + "version": "3.5.0", + "resolved": "https://registry.npmmirror.com/fast-redact/-/fast-redact-3.5.0.tgz", + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-uri": { + "version": "2.4.0", + "resolved": "https://registry.npmmirror.com/fast-uri/-/fast-uri-2.4.0.tgz", + "integrity": "sha512-ypuAmmMKInk5q7XcepxlnUWDLWv4GFtaJqAzWKqn62IpQ3pejtr5dTVbt3vwqVaMKmkNR55sTT+CqUKIaT21BA==", + "license": "MIT" + }, + "node_modules/fastify": { + "version": "4.29.1", + "resolved": "https://registry.npmmirror.com/fastify/-/fastify-4.29.1.tgz", + "integrity": "sha512-m2kMNHIG92tSNWv+Z3UeTR9AWLLuo7KctC7mlFPtMEVrfjIhmQhkQnT9v15qA/BfVq3vvj134Y0jl9SBje3jXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^3.5.0", + "@fastify/error": "^3.4.0", + "@fastify/fast-json-stringify-compiler": "^4.3.0", + "abstract-logging": "^2.0.1", + "avvio": "^8.3.0", + "fast-content-type-parse": "^1.1.0", + "fast-json-stringify": "^5.8.0", + "find-my-way": "^8.0.0", + "light-my-request": "^5.11.0", + "pino": "^9.0.0", + "process-warning": "^3.0.0", + "proxy-addr": "^2.0.7", + "rfdc": "^1.3.0", + "secure-json-parse": "^2.7.0", + "semver": "^7.5.4", + "toad-cache": "^3.3.0" + } + }, + "node_modules/fastify-plugin": { + "version": "4.5.1", + "resolved": "https://registry.npmmirror.com/fastify-plugin/-/fastify-plugin-4.5.1.tgz", + "integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==", + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/find-my-way": { + "version": "8.2.2", + "resolved": "https://registry.npmmirror.com/find-my-way/-/find-my-way-8.2.2.tgz", + "integrity": "sha512-Dobi7gcTEq8yszimcfp/R7+owiT4WncAJ7VTTgFH1jYJ5GaG1FbhjwDG820hptN0QDFvzVY3RfCzdInvGPGzjA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^3.1.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/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-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/json-schema-ref-resolver": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz", + "integrity": "sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/light-my-request": { + "version": "5.14.0", + "resolved": "https://registry.npmmirror.com/light-my-request/-/light-my-request-5.14.0.tgz", + "integrity": "sha512-aORPWntbpH5esaYpGOOmri0OHDOe3wC5M2MQxZ9dvMLZm6DnaAn0kJlcbU9hwsQgLzmZyReKwFwwPkR+nHu5kA==", + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^0.7.0", + "process-warning": "^3.0.0", + "set-cookie-parser": "^2.4.1" + } + }, + "node_modules/mnemonist": { + "version": "0.39.6", + "resolved": "https://registry.npmmirror.com/mnemonist/-/mnemonist-0.39.6.tgz", + "integrity": "sha512-A/0v5Z59y63US00cRSLiloEIw3t5G+MiKz4BhX21FI+YBJXBOGW0ohFxTxO08dsOYlzxo87T7vGfZKYp2bcAWA==", + "license": "MIT", + "dependencies": { + "obliterator": "^2.0.1" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/obliterator": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/obliterator/-/obliterator-2.0.5.tgz", + "integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==", + "license": "MIT" + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/pino": { + "version": "9.11.0", + "resolved": "https://registry.npmmirror.com/pino/-/pino-9.11.0.tgz", + "integrity": "sha512-+YIodBB9sxcWeR8PrXC2K3gEDyfkUuVEITOcbqrfcj+z5QW4ioIcqZfYFbrLTYLsmAwunbS7nfU/dpBB6PZc1g==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", + "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", + "license": "MIT" + }, + "node_modules/pino/node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/postgres": { + "version": "3.4.7", + "resolved": "https://registry.npmmirror.com/postgres/-/postgres-3.4.7.tgz", + "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==", + "license": "Unlicense", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + }, + "node_modules/process-warning": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/ret": { + "version": "0.4.3", + "resolved": "https://registry.npmmirror.com/ret/-/ret-0.4.3.tgz", + "integrity": "sha512-0f4Memo5QP7WQyUEAYUO3esD/XjOc3Zjjg5CPsAq1p8sIu0XPeMbHJemKA0BO7tV0X7+A0FoEpbmHXWxPyD3wQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/safe-regex2": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/safe-regex2/-/safe-regex2-3.1.0.tgz", + "integrity": "sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug==", + "license": "MIT", + "dependencies": { + "ret": "~0.4.0" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmmirror.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmmirror.com/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, + "node_modules/sonic-boom": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/sonic-boom/-/sonic-boom-4.2.0.tgz", + "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/toad-cache": { + "version": "3.7.0", + "resolved": "https://registry.npmmirror.com/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/tsx": { + "version": "4.20.5", + "resolved": "https://registry.npmmirror.com/tsx/-/tsx-4.20.5.tgz", + "integrity": "sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", + "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.10.tgz", + "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", + "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.10.tgz", + "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", + "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", + "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", + "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", + "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", + "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", + "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", + "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.25.10", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", + "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.10", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", + "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", + "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.10", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", + "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.25.10", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", + "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", + "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", + "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", + "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", + "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", + "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", + "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", + "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.25.10", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.10.tgz", + "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.10", + "@esbuild/android-arm": "0.25.10", + "@esbuild/android-arm64": "0.25.10", + "@esbuild/android-x64": "0.25.10", + "@esbuild/darwin-arm64": "0.25.10", + "@esbuild/darwin-x64": "0.25.10", + "@esbuild/freebsd-arm64": "0.25.10", + "@esbuild/freebsd-x64": "0.25.10", + "@esbuild/linux-arm": "0.25.10", + "@esbuild/linux-arm64": "0.25.10", + "@esbuild/linux-ia32": "0.25.10", + "@esbuild/linux-loong64": "0.25.10", + "@esbuild/linux-mips64el": "0.25.10", + "@esbuild/linux-ppc64": "0.25.10", + "@esbuild/linux-riscv64": "0.25.10", + "@esbuild/linux-s390x": "0.25.10", + "@esbuild/linux-x64": "0.25.10", + "@esbuild/netbsd-arm64": "0.25.10", + "@esbuild/netbsd-x64": "0.25.10", + "@esbuild/openbsd-arm64": "0.25.10", + "@esbuild/openbsd-x64": "0.25.10", + "@esbuild/openharmony-arm64": "0.25.10", + "@esbuild/sunos-x64": "0.25.10", + "@esbuild/win32-arm64": "0.25.10", + "@esbuild/win32-ia32": "0.25.10", + "@esbuild/win32-x64": "0.25.10" + } + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmmirror.com/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c1a2b5e --- /dev/null +++ b/package.json @@ -0,0 +1,38 @@ +{ + "name": "accounts-manager", + "version": "1.0.0", + "description": "Lightweight account management platform", + "main": "dist/index.js", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "db:create": "tsx src/scripts/createDatabase.ts", + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate", + "db:studio": "drizzle-kit studio", + "db:setup": "npm run db:create && npm run db:migrate" + }, + "keywords": [ + "account", + "management", + "fastify", + "drizzle" + ], + "author": "", + "license": "ISC", + "dependencies": { + "@fastify/cors": "^9.0.1", + "dotenv": "^17.2.2", + "drizzle-orm": "^0.33.0", + "fastify": "^4.28.1", + "postgres": "^3.4.4", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/node": "^20.14.9", + "drizzle-kit": "^0.24.0", + "tsx": "^4.16.0", + "typescript": "^5.5.2" + } +} diff --git a/src/api/v1/script/actions.ts b/src/api/v1/script/actions.ts new file mode 100644 index 0000000..94ea4eb --- /dev/null +++ b/src/api/v1/script/actions.ts @@ -0,0 +1,145 @@ +import { FastifyPluginAsync } from 'fastify'; +import { z } from 'zod'; +import { accountService } from '../../../core/AccountService'; +import { + createSuccessResponse, + createNoResourceResponse, + createInvalidParamsResponse, + createPermissionDeniedResponse, + createBusinessErrorResponse, +} from '../../../lib/apiResponse'; +import { ScriptUploadItem } from '../../../types/api'; + +const acquireQuerySchema = z.object({ + platform: z.string().min(1), + count: z.coerce.number().int().min(1).max(100).default(1), +}); + +const updateParamsSchema = z.object({ + ownerId: z.string().min(1), + accountId: z.coerce.number().int().positive(), + newStatus: z.string().min(1), +}); + +const updateQuerySchema = z.object({ + notes: z.string().optional(), +}); + +const uploadParamsSchema = z.object({ + ownerId: z.string().min(1), +}); + +const uploadBodySchema = z.array( + z.object({ + platform: z.string().min(1), + customId: z.string().min(1), + data: z.string().min(1), + status: z.string().optional(), + }) +); + +const scriptActions: FastifyPluginAsync = async function (fastify) { + fastify.get<{ + Params: { ownerId: string }; + Querystring: z.infer; + }>('/s/v1/:ownerId/acquire', async (request, reply) => { + try { + const { ownerId } = request.params; + const query = acquireQuerySchema.parse(request.query); + + const accounts = await accountService.acquireAccounts( + ownerId, + query.platform, + query.count + ); + + if (accounts.length === 0) { + return reply.send( + createNoResourceResponse( + `No available accounts found for platform '${query.platform}'.` + ) + ); + } + + return reply.send( + createSuccessResponse( + accounts, + `Successfully acquired ${accounts.length} account${accounts.length > 1 ? 's' : ''}.` + ) + ); + } catch (error) { + if (error instanceof z.ZodError) { + return reply.send(createInvalidParamsResponse('Invalid query parameters.')); + } + fastify.log.error(error); + return reply.send(createBusinessErrorResponse('Failed to acquire accounts.')); + } + }); + + fastify.get<{ + Params: z.infer; + Querystring: z.infer; + }>('/s/v1/:ownerId/update/:accountId/:newStatus', async (request, reply) => { + try { + const params = updateParamsSchema.parse(request.params); + const query = updateQuerySchema.parse(request.query); + + const success = await accountService.updateAccountStatus( + params.accountId, + params.ownerId, + params.newStatus, + query.notes + ); + + if (!success) { + return reply + .code(403) + .send(createPermissionDeniedResponse('Account not found or permission denied.')); + } + + return reply.send( + createSuccessResponse( + { updatedId: params.accountId }, + `Account status updated to '${params.newStatus}'.` + ) + ); + } catch (error) { + if (error instanceof z.ZodError) { + return reply.send(createInvalidParamsResponse('Invalid parameters.')); + } + fastify.log.error(error); + return reply.send(createBusinessErrorResponse('Failed to update account status.')); + } + }); + + fastify.post<{ + Params: z.infer; + Body: ScriptUploadItem[]; + }>('/s/v1/:ownerId/upload', async (request, reply) => { + try { + const { ownerId } = uploadParamsSchema.parse(request.params); + const items = uploadBodySchema.parse(request.body); + + if (items.length === 0) { + return reply.send(createInvalidParamsResponse('No items to upload.')); + } + + const result = await accountService.uploadAccounts(ownerId, items); + + return reply.send( + createSuccessResponse( + result, + `Successfully processed ${result.processedCount} accounts (${result.createdCount} created, ${result.updatedCount} updated).` + ) + ); + } catch (error) { + if (error instanceof z.ZodError) { + return reply.send(createInvalidParamsResponse('Invalid request body.')); + } + fastify.log.error(error); + return reply.send(createBusinessErrorResponse('Failed to upload accounts.')); + } + }); +}; + +export default scriptActions; \ No newline at end of file diff --git a/src/api/v1/web/accounts.ts b/src/api/v1/web/accounts.ts new file mode 100644 index 0000000..bae1d8d --- /dev/null +++ b/src/api/v1/web/accounts.ts @@ -0,0 +1,129 @@ +import { FastifyPluginAsync } from 'fastify'; +import { z } from 'zod'; +import { accountService } from '../../../core/AccountService'; +import { + createSuccessResponse, + createInvalidParamsResponse, + createBusinessErrorResponse, +} from '../../../lib/apiResponse'; +import { + ListAccountsBody, + BatchDeleteBody, + BatchUpdateBody, +} from '../../../types/api'; + +const listAccountsBodySchema = z.object({ + filters: z.object({ + platform: z.string().optional(), + status: z.array(z.string()).optional(), + ownerId: z.string().optional(), + search: z.string().optional(), + }), + pagination: z.object({ + page: z.number().int().min(1), + pageSize: z.number().int().min(1).max(10000), + }), + sort: z.object({ + field: z.enum([ + 'id', + 'ownerId', + 'platform', + 'customId', + 'data', + 'status', + 'notes', + 'lockedAt', + 'createdAt', + 'updatedAt', + ]), + order: z.enum(['asc', 'desc']), + }), +}); + +const batchDeleteBodySchema = z.object({ + ids: z.array(z.number().int().positive()).min(1), +}); + +const batchUpdateBodySchema = z.object({ + ids: z.array(z.number().int().positive()).min(1), + payload: z.object({ + status: z.string().optional(), + ownerId: z.string().optional(), + notes: z.string().optional(), + }), +}); + +const accountsRoutes: FastifyPluginAsync = async function (fastify) { + fastify.post<{ + Body: ListAccountsBody; + }>('/web/v1/accounts/list', async (request, reply) => { + try { + const body = listAccountsBodySchema.parse(request.body); + + const result = await accountService.listAccounts( + body.filters, + body.pagination, + body.sort + ); + + return reply.send(createSuccessResponse(result)); + } catch (error) { + if (error instanceof z.ZodError) { + return reply.send(createInvalidParamsResponse('Invalid request body.')); + } + fastify.log.error(error); + return reply.send(createBusinessErrorResponse('Failed to list accounts.')); + } + }); + + fastify.post<{ + Body: BatchDeleteBody; + }>('/web/v1/accounts/delete-batch', async (request, reply) => { + try { + const body = batchDeleteBodySchema.parse(request.body); + + const deletedCount = await accountService.batchDeleteAccounts(body.ids); + + return reply.send( + createSuccessResponse( + { deletedCount }, + `Successfully deleted ${deletedCount} accounts.` + ) + ); + } catch (error) { + if (error instanceof z.ZodError) { + return reply.send(createInvalidParamsResponse('Invalid request body.')); + } + fastify.log.error(error); + return reply.send(createBusinessErrorResponse('Failed to delete accounts.')); + } + }); + + fastify.post<{ + Body: BatchUpdateBody; + }>('/web/v1/accounts/update-batch', async (request, reply) => { + try { + const body = batchUpdateBodySchema.parse(request.body); + + const updatedCount = await accountService.batchUpdateAccounts( + body.ids, + body.payload + ); + + return reply.send( + createSuccessResponse( + { updatedCount }, + `Successfully updated ${updatedCount} accounts.` + ) + ); + } catch (error) { + if (error instanceof z.ZodError) { + return reply.send(createInvalidParamsResponse('Invalid request body.')); + } + fastify.log.error(error); + return reply.send(createBusinessErrorResponse('Failed to update accounts.')); + } + }); +}; + +export default accountsRoutes; \ No newline at end of file diff --git a/src/api/v1/web/stats.ts b/src/api/v1/web/stats.ts new file mode 100644 index 0000000..7fbb400 --- /dev/null +++ b/src/api/v1/web/stats.ts @@ -0,0 +1,21 @@ +import { FastifyPluginAsync } from 'fastify'; +import { accountService } from '../../../core/AccountService'; +import { + createSuccessResponse, + createBusinessErrorResponse, +} from '../../../lib/apiResponse'; + +const statsRoutes: FastifyPluginAsync = async function (fastify) { + fastify.get('/web/v1/stats/overview', async (request, reply) => { + try { + const stats = await accountService.getStatsOverview(); + + return reply.send(createSuccessResponse(stats)); + } catch (error) { + fastify.log.error(error); + return reply.send(createBusinessErrorResponse('Failed to get stats overview.')); + } + }); +}; + +export default statsRoutes; \ No newline at end of file diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..b1f7451 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,14 @@ +import 'dotenv/config' + +export const config = { + database: { + url: process.env.DATABASE_URL || 'postgresql://localhost:5432/accounts_db', + }, + server: { + port: parseInt(process.env.PORT || '3000', 10), + host: '0.0.0.0', + }, + lockTimeout: { + minutes: parseInt(process.env.LOCK_TIMEOUT_MINUTES || '5', 10), + }, +}; \ No newline at end of file diff --git a/src/core/AccountService.ts b/src/core/AccountService.ts new file mode 100644 index 0000000..b0e6be2 --- /dev/null +++ b/src/core/AccountService.ts @@ -0,0 +1,299 @@ +import { eq, and, sql, or, ilike, inArray } from 'drizzle-orm'; +import { db } from '../db/index'; +import { accounts } from '../db/schema'; +import { Account } from '../types/index'; +import { + ScriptUploadItem, + ListAccountsFilters, + PaginationResult, + StatsOverview, +} from '../types/api'; +import { config } from '../config'; + +export class AccountService { + async acquireAccounts( + ownerId: string, + platform: string, + count: number = 1 + ): Promise[]> { + return await db.transaction(async (tx) => { + const availableAccounts = await tx + .select({ id: accounts.id, customId: accounts.customId, data: accounts.data }) + .from(accounts) + .where( + and( + eq(accounts.ownerId, ownerId), + eq(accounts.platform, platform), + eq(accounts.status, 'available') + ) + ) + .limit(count) + .for('update'); + + if (availableAccounts.length === 0) { + return []; + } + + const accountIds = availableAccounts.map((acc) => acc.id); + await tx + .update(accounts) + .set({ + status: 'locked', + lockedAt: new Date(), + updatedAt: new Date(), + }) + .where(inArray(accounts.id, accountIds)); + + return availableAccounts; + }); + } + + async updateAccountStatus( + accountId: number, + ownerId: string, + newStatus: string, + notes?: string + ): Promise { + const updateData: Partial = { + status: newStatus, + updatedAt: new Date(), + }; + + if (newStatus !== 'locked') { + updateData.lockedAt = null; + } + + if (notes) { + updateData.notes = notes; + } + + const result = await db + .update(accounts) + .set(updateData) + .where(and(eq(accounts.id, accountId), eq(accounts.ownerId, ownerId))) + .returning({ id: accounts.id }); + + return result.length > 0; + } + + async uploadAccounts( + ownerId: string, + items: ScriptUploadItem[] + ): Promise<{ processedCount: number; createdCount: number; updatedCount: number }> { + let createdCount = 0; + let updatedCount = 0; + + for (const item of items) { + const existingAccount = await db + .select({ id: accounts.id }) + .from(accounts) + .where( + and( + eq(accounts.platform, item.platform), + eq(accounts.customId, item.customId) + ) + ) + .limit(1); + + if (existingAccount.length > 0) { + await db + .update(accounts) + .set({ + data: item.data, + status: item.status || 'available', + ownerId, + updatedAt: new Date(), + }) + .where(eq(accounts.id, existingAccount[0].id)); + updatedCount++; + } else { + await db.insert(accounts).values({ + ownerId, + platform: item.platform, + customId: item.customId, + data: item.data, + status: item.status || 'available', + }); + createdCount++; + } + } + + return { + processedCount: items.length, + createdCount, + updatedCount, + }; + } + + async listAccounts( + filters: ListAccountsFilters, + pagination: { page: number; pageSize: number }, + sort: { field: keyof Account; order: 'asc' | 'desc' } + ): Promise<{ list: Account[]; pagination: PaginationResult }> { + const conditions: any[] = []; + + if (filters.platform) { + conditions.push(eq(accounts.platform, filters.platform)); + } + + if (filters.status && filters.status.length > 0) { + conditions.push(inArray(accounts.status, filters.status)); + } + + if (filters.ownerId) { + conditions.push(eq(accounts.ownerId, filters.ownerId)); + } + + if (filters.search) { + conditions.push( + or( + ilike(accounts.customId, `%${filters.search}%`), + ilike(accounts.notes, `%${filters.search}%`) + ) + ); + } + + const whereClause = conditions.length > 0 ? and(...conditions) : undefined; + + const [totalResult] = await db + .select({ count: sql`count(*)` }) + .from(accounts) + .where(whereClause); + + const total = Number(totalResult.count); + const totalPages = Math.ceil(total / pagination.pageSize); + const offset = (pagination.page - 1) * pagination.pageSize; + + const orderColumn = accounts[sort.field]; + const orderClause = sort.order === 'desc' ? sql`${orderColumn} DESC` : sql`${orderColumn} ASC`; + + const list = await db + .select() + .from(accounts) + .where(whereClause) + .orderBy(orderClause) + .limit(pagination.pageSize) + .offset(offset); + + return { + list, + pagination: { + page: pagination.page, + pageSize: pagination.pageSize, + total, + totalPages, + }, + }; + } + + async batchDeleteAccounts(ids: number[]): Promise { + const result = await db + .delete(accounts) + .where(inArray(accounts.id, ids)) + .returning({ id: accounts.id }); + + return result.length; + } + + async batchUpdateAccounts( + ids: number[], + payload: Partial> + ): Promise { + const updateData = { ...payload, updatedAt: new Date() }; + + const result = await db + .update(accounts) + .set(updateData) + .where(inArray(accounts.id, ids)) + .returning({ id: accounts.id }); + + return result.length; + } + + async getStatsOverview(): Promise { + const [totalResult] = await db + .select({ count: sql`count(*)` }) + .from(accounts); + + const platformStats = await db + .select({ + platform: accounts.platform, + count: sql`count(*)`, + }) + .from(accounts) + .groupBy(accounts.platform); + + const ownerStats = await db + .select({ + ownerId: accounts.ownerId, + count: sql`count(*)`, + }) + .from(accounts) + .groupBy(accounts.ownerId); + + const statusStats = await db + .select({ + status: accounts.status, + count: sql`count(*)`, + }) + .from(accounts) + .groupBy(accounts.status); + + const detailedStats = await db + .select({ + platform: accounts.platform, + ownerId: accounts.ownerId, + status: accounts.status, + count: sql`count(*)`, + }) + .from(accounts) + .groupBy(accounts.platform, accounts.ownerId, accounts.status); + + return { + totalAccounts: Number(totalResult.count), + platformSummary: platformStats.reduce((acc, item) => { + acc[item.platform] = Number(item.count); + return acc; + }, {} as Record), + ownerSummary: ownerStats.reduce((acc, item) => { + acc[item.ownerId] = Number(item.count); + return acc; + }, {} as Record), + statusSummary: statusStats.reduce((acc, item) => { + acc[item.status] = Number(item.count); + return acc; + }, {} as Record), + detailedBreakdown: detailedStats.map((item) => ({ + platform: item.platform, + ownerId: item.ownerId, + status: item.status, + count: Number(item.count), + })), + }; + } + + async cleanupStaleLocks(): Promise { + const thresholdTime = new Date( + Date.now() - config.lockTimeout.minutes * 60 * 1000 + ); + + const result = await db + .update(accounts) + .set({ + status: 'available', + lockedAt: null, + updatedAt: new Date(), + }) + .where( + and( + eq(accounts.status, 'locked'), + sql`${accounts.lockedAt} < ${thresholdTime.toISOString()}` + ) + ) + .returning({ id: accounts.id }); + + return result.length; + } +} + +export const accountService = new AccountService(); \ No newline at end of file diff --git a/src/db/index.ts b/src/db/index.ts new file mode 100644 index 0000000..db5a95d --- /dev/null +++ b/src/db/index.ts @@ -0,0 +1,7 @@ +import { drizzle } from 'drizzle-orm/postgres-js'; +import postgres from 'postgres'; +import { config } from '../config'; +import * as schema from './schema'; + +const client = postgres(config.database.url); +export const db = drizzle(client, { schema }); \ No newline at end of file diff --git a/src/db/schema.ts b/src/db/schema.ts new file mode 100644 index 0000000..ac19148 --- /dev/null +++ b/src/db/schema.ts @@ -0,0 +1,18 @@ +import { pgTable, serial, varchar, timestamp, uniqueIndex, text, index } from 'drizzle-orm/pg-core'; + +export const accounts = pgTable('accounts', { + id: serial('id').primaryKey(), + ownerId: varchar('owner_id', { length: 128 }).notNull(), + platform: varchar('platform', { length: 100 }).notNull(), + customId: varchar('custom_id', { length: 255 }).notNull(), + data: text('data').notNull(), + status: varchar('status', { length: 50 }).notNull(), + notes: text('notes'), + lockedAt: timestamp('locked_at'), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().notNull(), +}, (table) => ({ + platformCustomIdIdx: uniqueIndex('platform_custom_id_idx').on(table.platform, table.customId), + ownerIdStatusIdx: index('owner_id_status_idx').on(table.ownerId, table.status), + platformOwnerIdx: index('platform_owner_idx').on(table.platform, table.ownerId), +})); \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..203d4f4 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,57 @@ +import fastify from 'fastify'; +import cors from '@fastify/cors'; +import { config } from './config'; +import { staleLockCleanup } from './jobs/staleLockCleanup'; + +import scriptActions from './api/v1/script/actions'; +import accountsRoutes from './api/v1/web/accounts'; +import statsRoutes from './api/v1/web/stats'; + +const server = fastify({ + logger: { + level: process.env.NODE_ENV === 'production' ? 'info' : 'debug', + }, +}); + +async function start() { + try { + await server.register(cors, { + origin: true, + credentials: true, + }); + + await server.register(scriptActions); + await server.register(accountsRoutes); + await server.register(statsRoutes); + + server.get('/health', async (request, reply) => { + return { status: 'ok', timestamp: new Date().toISOString() }; + }); + + staleLockCleanup.start(1); + + const gracefulShutdown = () => { + console.log('Received shutdown signal, stopping services...'); + staleLockCleanup.stop(); + server.close().then(() => { + console.log('Server stopped successfully'); + process.exit(0); + }); + }; + + process.on('SIGTERM', gracefulShutdown); + process.on('SIGINT', gracefulShutdown); + + await server.listen({ + port: config.server.port, + host: config.server.host, + }); + + console.log(`Server is running on http://${config.server.host}:${config.server.port}`); + } catch (error) { + server.log.error(error); + process.exit(1); + } +} + +start(); \ No newline at end of file diff --git a/src/jobs/staleLockCleanup.ts b/src/jobs/staleLockCleanup.ts new file mode 100644 index 0000000..1896ee2 --- /dev/null +++ b/src/jobs/staleLockCleanup.ts @@ -0,0 +1,59 @@ +import { accountService } from '../core/AccountService'; + +export class StaleLockCleanup { + private intervalId: NodeJS.Timeout | null = null; + private isRunning = false; + + start(intervalMinutes: number = 1): void { + if (this.isRunning) { + console.log('Stale lock cleanup job is already running'); + return; + } + + this.isRunning = true; + const intervalMs = intervalMinutes * 60 * 1000; + + console.log(`Starting stale lock cleanup job with ${intervalMinutes} minute(s) interval`); + + this.intervalId = setInterval(async () => { + try { + const cleanedCount = await accountService.cleanupStaleLocks(); + if (cleanedCount > 0) { + console.log(`Stale lock cleanup: Released ${cleanedCount} locked accounts`); + } + } catch (error) { + console.error('Error during stale lock cleanup:', error); + } + }, intervalMs); + + this.runOnce(); + } + + stop(): void { + if (this.intervalId) { + clearInterval(this.intervalId); + this.intervalId = null; + } + this.isRunning = false; + console.log('Stale lock cleanup job stopped'); + } + + async runOnce(): Promise { + try { + const cleanedCount = await accountService.cleanupStaleLocks(); + if (cleanedCount > 0) { + console.log(`Manual stale lock cleanup: Released ${cleanedCount} locked accounts`); + } + return cleanedCount; + } catch (error) { + console.error('Error during manual stale lock cleanup:', error); + throw error; + } + } + + isJobRunning(): boolean { + return this.isRunning; + } +} + +export const staleLockCleanup = new StaleLockCleanup(); \ No newline at end of file diff --git a/src/lib/apiResponse.ts b/src/lib/apiResponse.ts new file mode 100644 index 0000000..2d44840 --- /dev/null +++ b/src/lib/apiResponse.ts @@ -0,0 +1,40 @@ +import { ApiResponse, BusinessCode } from '../types/api'; + +export function createSuccessResponse(data: T, message = 'Success'): ApiResponse { + return { + code: BusinessCode.Success, + message, + data, + }; +} + +export function createErrorResponse( + code: BusinessCode, + message: string +): ApiResponse { + return { + code, + message, + data: null, + }; +} + +export function createNoResourceResponse(message = 'No resource found'): ApiResponse { + return createErrorResponse(BusinessCode.NoResource, message); +} + +export function createInvalidParamsResponse(message = 'Invalid parameters'): ApiResponse { + return createErrorResponse(BusinessCode.InvalidParams, message); +} + +export function createResourceConflictResponse(message = 'Resource conflict'): ApiResponse { + return createErrorResponse(BusinessCode.ResourceConflict, message); +} + +export function createPermissionDeniedResponse(message = 'Permission denied'): ApiResponse { + return createErrorResponse(BusinessCode.PermissionDenied, message); +} + +export function createBusinessErrorResponse(message = 'Business error'): ApiResponse { + return createErrorResponse(BusinessCode.BusinessError, message); +} \ No newline at end of file diff --git a/src/scripts/createDatabase.ts b/src/scripts/createDatabase.ts new file mode 100644 index 0000000..718f368 --- /dev/null +++ b/src/scripts/createDatabase.ts @@ -0,0 +1,51 @@ +import postgres from 'postgres'; +import { config } from '../config'; + +async function createDatabase() { + const dbUrl = new URL(config.database.url); + const dbName = dbUrl.pathname.slice(1); // 去掉开头的 '/' + + // 创建连接到 postgres 默认数据库的连接 + const adminDbUrl = config.database.url.replace(`/${dbName}`, '/postgres'); + const adminSql = postgres(adminDbUrl); + + try { + console.log(`Checking if database '${dbName}' exists...`); + + // 检查数据库是否存在 + const result = await adminSql` + SELECT 1 FROM pg_database WHERE datname = ${dbName} + `; + + if (result.length === 0) { + console.log(`Creating database '${dbName}'...`); + + // 创建数据库 + await adminSql.unsafe(`CREATE DATABASE "${dbName}"`); + + console.log(`✅ Database '${dbName}' created successfully!`); + } else { + console.log(`✅ Database '${dbName}' already exists.`); + } + } catch (error) { + console.error('❌ Error creating database:', error); + throw error; + } finally { + await adminSql.end(); + } +} + +// 如果直接运行此脚本 +if (require.main === module) { + createDatabase() + .then(() => { + console.log('Database initialization completed.'); + process.exit(0); + }) + .catch((error) => { + console.error('Database initialization failed:', error); + process.exit(1); + }); +} + +export { createDatabase }; \ No newline at end of file diff --git a/src/types/api.ts b/src/types/api.ts new file mode 100644 index 0000000..c87ce82 --- /dev/null +++ b/src/types/api.ts @@ -0,0 +1,78 @@ +import { Account } from './index'; + +export enum BusinessCode { + Success = 0, + NoResource = 1001, + InvalidParams = 2001, + ResourceConflict = 3001, + PermissionDenied = 4001, + BusinessError = 5001, +} + +export interface ApiResponse { + code: BusinessCode; + message: string; + data: T | null; +} + +export interface PaginationResult { + page: number; + pageSize: number; + total: number; + totalPages: number; +} + +export type ScriptAcquireResponse = Pick[]; + +export interface ScriptUploadItem { + platform: string; + customId: string; + data: string; + status?: string; +} + +export interface ListAccountsFilters { + platform?: string; + status?: string[]; + ownerId?: string; + search?: string; +} + +export interface ListAccountsBody { + filters: ListAccountsFilters; + pagination: { + page: number; + pageSize: number; + }; + sort: { + field: keyof Account; + order: 'asc' | 'desc'; + }; +} + +export interface ListAccountsResponse { + list: Account[]; + pagination: PaginationResult; +} + +export interface BatchDeleteBody { + ids: number[]; +} + +export interface BatchUpdateBody { + ids: number[]; + payload: Partial>; +} + +export interface StatsOverview { + totalAccounts: number; + platformSummary: Record; + ownerSummary: Record; + statusSummary: Record; + detailedBreakdown: { + platform: string; + ownerId: string; + status: string; + count: number; + }[]; +} \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..fe612d2 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,4 @@ +import { accounts } from '../db/schema'; +import { InferSelectModel } from 'drizzle-orm'; + +export type Account = InferSelectModel; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..986e443 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "CommonJS", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} \ No newline at end of file