feat: add Turso (libSQL) storage support for EdgeOne deployment
- Add TursoAdapter implementing DatabaseAdapter interface using @libsql/client - Add 'turso' storage type to db.ts storage selector (reuses D1Storage) - Add init-turso.js script for database migration and admin initialization - Add @libsql/client dependency to package.json - Update next.config.js to exclude @libsql/client from client-side bundle - Update edgeone-deploy.yml workflow with TURSO_URL and TURSO_TOKEN secrets - Update edgeone-build.mjs runtime env keys to include Turso variables - Update .env.edgeone.example with Turso deployment instructions - Update README.md with Turso as recommended storage for EdgeOne Turso provides free SQLite-compatible cloud database (libSQL) with: - 500 databases, 9GB storage, 1B row reads/month on free tier - HTTP API compatible with edge/serverless environments - Full SQLite syntax compatibility, reuses existing migrations
This commit is contained in:
+12
-1
@@ -6,7 +6,18 @@ BUILD_TARGET=edgeone
|
|||||||
EDGEONE_PAGES=1
|
EDGEONE_PAGES=1
|
||||||
|
|
||||||
# 默认不设置 NEXT_PUBLIC_STORAGE_TYPE,项目会使用默认 localstorage。
|
# 默认不设置 NEXT_PUBLIC_STORAGE_TYPE,项目会使用默认 localstorage。
|
||||||
# 如需云端持久化,再自行改为 upstash / redis / postgres 等项目支持的存储。
|
# 如需云端持久化,可选用以下存储方案:
|
||||||
|
#
|
||||||
|
# 方案一:Turso(推荐,免费 SQLite 云数据库)
|
||||||
|
# 1. 注册 https://turso.tech 获取免费数据库
|
||||||
|
# 2. 设置 NEXT_PUBLIC_STORAGE_TYPE=turso
|
||||||
|
# 3. 运行 pnpm init:turso 初始化数据库表结构
|
||||||
|
# 4. 配置 TURSO_URL 和 TURSO_TOKEN
|
||||||
|
#
|
||||||
|
# 方案二:Upstash Redis(KV 存储)
|
||||||
|
# 1. 注册 https://upstash.com 获取免费 Redis
|
||||||
|
# 2. 设置 NEXT_PUBLIC_STORAGE_TYPE=upstash
|
||||||
|
# 3. 配置 UPSTASH_URL 和 UPSTASH_TOKEN
|
||||||
|
|
||||||
# 管理员账号(请替换)
|
# 管理员账号(请替换)
|
||||||
USERNAME=admin
|
USERNAME=admin
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ jobs:
|
|||||||
PASSWORD: ${{ secrets.PASSWORD }}
|
PASSWORD: ${{ secrets.PASSWORD }}
|
||||||
UPSTASH_URL: ${{ secrets.UPSTASH_URL }}
|
UPSTASH_URL: ${{ secrets.UPSTASH_URL }}
|
||||||
UPSTASH_TOKEN: ${{ secrets.UPSTASH_TOKEN }}
|
UPSTASH_TOKEN: ${{ secrets.UPSTASH_TOKEN }}
|
||||||
|
TURSO_URL: ${{ secrets.TURSO_URL }}
|
||||||
|
TURSO_TOKEN: ${{ secrets.TURSO_TOKEN }}
|
||||||
TMDB_API_KEY: ${{ secrets.TMDB_API_KEY }}
|
TMDB_API_KEY: ${{ secrets.TMDB_API_KEY }}
|
||||||
TMDB_IMAGE_DOMAIN: ${{ secrets.TMDB_IMAGE_DOMAIN }}
|
TMDB_IMAGE_DOMAIN: ${{ secrets.TMDB_IMAGE_DOMAIN }}
|
||||||
TMDB_PROXY: ${{ secrets.TMDB_PROXY }}
|
TMDB_PROXY: ${{ secrets.TMDB_PROXY }}
|
||||||
@@ -135,7 +137,7 @@ jobs:
|
|||||||
NEXT_PUBLIC_STORAGE_PREFIX NEXT_PUBLIC_ENABLE_STREAM_PROXY NEXT_PUBLIC_STREAM_PROXY_URL
|
NEXT_PUBLIC_STORAGE_PREFIX NEXT_PUBLIC_ENABLE_STREAM_PROXY NEXT_PUBLIC_STREAM_PROXY_URL
|
||||||
NEXT_PUBLIC_ENABLE_TURNSTILE NEXT_PUBLIC_TURNSTILE_SITE_KEY
|
NEXT_PUBLIC_ENABLE_TURNSTILE NEXT_PUBLIC_TURNSTILE_SITE_KEY
|
||||||
NEXT_PUBLIC_THEME NEXT_PUBLIC_THEME_COLOR NEXT_PUBLIC_APP_URL NEXT_PUBLIC_BASE_PATH
|
NEXT_PUBLIC_THEME NEXT_PUBLIC_THEME_COLOR NEXT_PUBLIC_APP_URL NEXT_PUBLIC_BASE_PATH
|
||||||
USERNAME PASSWORD UPSTASH_URL UPSTASH_TOKEN
|
USERNAME PASSWORD UPSTASH_URL UPSTASH_TOKEN TURSO_URL TURSO_TOKEN
|
||||||
TMDB_API_KEY TMDB_IMAGE_DOMAIN TMDB_PROXY TMDB_REVERSE_PROXY
|
TMDB_API_KEY TMDB_IMAGE_DOMAIN TMDB_PROXY TMDB_REVERSE_PROXY
|
||||||
DANMAKU_API_BASE DANMAKU_API_TOKEN ANNOUNCEMENT CRON_PASSWORD SITE_BASE
|
DANMAKU_API_BASE DANMAKU_API_TOKEN ANNOUNCEMENT CRON_PASSWORD SITE_BASE
|
||||||
REDIS_URL KVROCKS_URL INIT_CONFIG CONFIG_SUBSCRIPTION_URL ENABLE_REGISTER
|
REDIS_URL KVROCKS_URL INIT_CONFIG CONFIG_SUBSCRIPTION_URL ENABLE_REGISTER
|
||||||
@@ -195,7 +197,7 @@ jobs:
|
|||||||
NEXT_PUBLIC_STORAGE_PREFIX NEXT_PUBLIC_ENABLE_STREAM_PROXY NEXT_PUBLIC_STREAM_PROXY_URL
|
NEXT_PUBLIC_STORAGE_PREFIX NEXT_PUBLIC_ENABLE_STREAM_PROXY NEXT_PUBLIC_STREAM_PROXY_URL
|
||||||
NEXT_PUBLIC_ENABLE_TURNSTILE NEXT_PUBLIC_TURNSTILE_SITE_KEY
|
NEXT_PUBLIC_ENABLE_TURNSTILE NEXT_PUBLIC_TURNSTILE_SITE_KEY
|
||||||
NEXT_PUBLIC_THEME NEXT_PUBLIC_THEME_COLOR NEXT_PUBLIC_APP_URL NEXT_PUBLIC_BASE_PATH
|
NEXT_PUBLIC_THEME NEXT_PUBLIC_THEME_COLOR NEXT_PUBLIC_APP_URL NEXT_PUBLIC_BASE_PATH
|
||||||
USERNAME PASSWORD UPSTASH_URL UPSTASH_TOKEN
|
USERNAME PASSWORD UPSTASH_URL UPSTASH_TOKEN TURSO_URL TURSO_TOKEN
|
||||||
TMDB_API_KEY TMDB_IMAGE_DOMAIN TMDB_PROXY TMDB_REVERSE_PROXY
|
TMDB_API_KEY TMDB_IMAGE_DOMAIN TMDB_PROXY TMDB_REVERSE_PROXY
|
||||||
DANMAKU_API_BASE DANMAKU_API_TOKEN ANNOUNCEMENT CRON_PASSWORD SITE_BASE
|
DANMAKU_API_BASE DANMAKU_API_TOKEN ANNOUNCEMENT CRON_PASSWORD SITE_BASE
|
||||||
REDIS_URL KVROCKS_URL INIT_CONFIG CONFIG_SUBSCRIPTION_URL ENABLE_REGISTER
|
REDIS_URL KVROCKS_URL INIT_CONFIG CONFIG_SUBSCRIPTION_URL ENABLE_REGISTER
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ EdgeOne Pages/Makers 支持通过 API Token 在 GitHub Actions 中自动构建
|
|||||||
|
|
||||||
1. 一个腾讯云 EdgeOne 账号
|
1. 一个腾讯云 EdgeOne 账号
|
||||||
2. Fork 本项目到你的 GitHub 账号
|
2. Fork 本项目到你的 GitHub 账号
|
||||||
3. 准备一个 Upstash Redis 实例(推荐)
|
3. 准备一个 Turso 数据库实例(推荐,免费 SQLite 云数据库)或 Upstash Redis 实例
|
||||||
4. 准备 EdgeOne API Token
|
4. 准备 EdgeOne API Token
|
||||||
|
|
||||||
#### 配置步骤
|
#### 配置步骤
|
||||||
@@ -224,7 +224,21 @@ EdgeOne Pages/Makers 支持通过 API Token 在 GitHub Actions 中自动构建
|
|||||||
| `EDGEONE_API_TOKEN` | EdgeOne API Token | `your_edgeone_token` |
|
| `EDGEONE_API_TOKEN` | EdgeOne API Token | `your_edgeone_token` |
|
||||||
| `USERNAME` | 站长账号 | `admin` |
|
| `USERNAME` | 站长账号 | `admin` |
|
||||||
| `PASSWORD` | 站长密码 | `your_secure_password` |
|
| `PASSWORD` | 站长密码 | `your_secure_password` |
|
||||||
| `NEXT_PUBLIC_STORAGE_TYPE` | 存储类型 | `upstash` |
|
| `NEXT_PUBLIC_STORAGE_TYPE` | 存储类型 | `turso` 或 `upstash` |
|
||||||
|
|
||||||
|
**使用 Turso(推荐):**
|
||||||
|
|
||||||
|
在 [Turso](https://turso.tech/) 注册账号并创建数据库,然后运行 `pnpm init:turso` 初始化表结构(需设置 TURSO_URL 和 TURSO_TOKEN 环境变量)。
|
||||||
|
|
||||||
|
| Secret 名称 | 说明 | 示例值 |
|
||||||
|
| -------------------------- | ------------------- | --------------------------------- |
|
||||||
|
| `TURSO_URL` | Turso 数据库 URL | `libsql://your-db.turso.io` |
|
||||||
|
| `TURSO_TOKEN` | Turso 访问令牌 | `your_turso_token` |
|
||||||
|
|
||||||
|
**使用 Upstash Redis:**
|
||||||
|
|
||||||
|
| Secret 名称 | 说明 | 示例值 |
|
||||||
|
| -------------------------- | ------------------- | ------------------------ |
|
||||||
| `UPSTASH_URL` | Upstash Redis URL | `https://xxx.upstash.io` |
|
| `UPSTASH_URL` | Upstash Redis URL | `https://xxx.upstash.io` |
|
||||||
| `UPSTASH_TOKEN` | Upstash Redis Token | `your_upstash_token` |
|
| `UPSTASH_TOKEN` | Upstash Redis Token | `your_upstash_token` |
|
||||||
|
|
||||||
@@ -458,11 +472,13 @@ dockge/komodo 等 docker compose UI 也有自动更新功能
|
|||||||
| NEXT_PUBLIC_SITE_NAME | 站点名称 | 任意字符串 | MoonTV |
|
| NEXT_PUBLIC_SITE_NAME | 站点名称 | 任意字符串 | MoonTV |
|
||||||
| ANNOUNCEMENT | 站点公告 | 任意字符串 | 本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。 |
|
| ANNOUNCEMENT | 站点公告 | 任意字符串 | 本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。 |
|
||||||
| ANNOUNCEMENT_DISPLAY_MODE | 公告显示模式 | once、every | once |
|
| ANNOUNCEMENT_DISPLAY_MODE | 公告显示模式 | once、every | once |
|
||||||
| NEXT_PUBLIC_STORAGE_TYPE | 播放记录/收藏的存储方式 | redis、kvrocks、upstash、d1 | 无默认,必填字段 |
|
| NEXT_PUBLIC_STORAGE_TYPE | 播放记录/收藏的存储方式 | redis、kvrocks、upstash、d1、turso、postgres | 无默认,必填字段 |
|
||||||
| KVROCKS_URL | kvrocks 连接 url | 连接 url | 空 |
|
| KVROCKS_URL | kvrocks 连接 url | 连接 url | 空 |
|
||||||
| REDIS_URL | redis 连接 url | 连接 url | 空 |
|
| REDIS_URL | redis 连接 url | 连接 url | 空 |
|
||||||
| UPSTASH_URL | upstash redis 连接 url | 连接 url | 空 |
|
| UPSTASH_URL | upstash redis 连接 url | 连接 url | 空 |
|
||||||
| UPSTASH_TOKEN | upstash redis 连接 token | 连接 token | 空 |
|
| UPSTASH_TOKEN | upstash redis 连接 token | 连接 token | 空 |
|
||||||
|
| TURSO_URL | Turso (libSQL) 数据库连接 url | libsql://xxx.turso.io | 空 |
|
||||||
|
| TURSO_TOKEN | Turso (libSQL) 数据库访问令牌 | 访问令牌 | 空 |
|
||||||
| NEXT_PUBLIC_SEARCH_MAX_PAGE | 搜索接口可拉取的最大页数 | 1-50 | 5 |
|
| NEXT_PUBLIC_SEARCH_MAX_PAGE | 搜索接口可拉取的最大页数 | 1-50 | 5 |
|
||||||
| NEXT_PUBLIC_DOUBAN_PROXY_TYPE | 豆瓣数据源请求方式 | 见下方 | direct |
|
| NEXT_PUBLIC_DOUBAN_PROXY_TYPE | 豆瓣数据源请求方式 | 见下方 | direct |
|
||||||
| NEXT_PUBLIC_DOUBAN_PROXY | 自定义豆瓣数据代理 URL | url prefix | (空) |
|
| NEXT_PUBLIC_DOUBAN_PROXY | 自定义豆瓣数据代理 URL | url prefix | (空) |
|
||||||
|
|||||||
+3
-1
@@ -136,13 +136,14 @@ const createNextConfig = (phase) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exclude better-sqlite3, D1, and Postgres modules from client-side bundle
|
// Exclude better-sqlite3, D1, Postgres, and Turso modules from client-side bundle
|
||||||
if (!isServer) {
|
if (!isServer) {
|
||||||
config.externals = config.externals || [];
|
config.externals = config.externals || [];
|
||||||
config.externals.push({
|
config.externals.push({
|
||||||
'better-sqlite3': 'commonjs better-sqlite3',
|
'better-sqlite3': 'commonjs better-sqlite3',
|
||||||
'@vercel/postgres': 'commonjs @vercel/postgres',
|
'@vercel/postgres': 'commonjs @vercel/postgres',
|
||||||
'pg': 'commonjs pg',
|
'pg': 'commonjs pg',
|
||||||
|
'@libsql/client': 'commonjs @libsql/client',
|
||||||
});
|
});
|
||||||
|
|
||||||
config.resolve.alias = {
|
config.resolve.alias = {
|
||||||
@@ -152,6 +153,7 @@ const createNextConfig = (phase) => {
|
|||||||
'@/lib/d1-adapter': false,
|
'@/lib/d1-adapter': false,
|
||||||
'@/lib/postgres.db': false,
|
'@/lib/postgres.db': false,
|
||||||
'@/lib/postgres-adapter': false,
|
'@/lib/postgres-adapter': false,
|
||||||
|
'@/lib/turso-adapter': false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
"watch-room:server": "node server/watch-room-standalone-server.js --port 3001 --auth YOUR_SECRET_KEY",
|
"watch-room:server": "node server/watch-room-standalone-server.js --port 3001 --auth YOUR_SECRET_KEY",
|
||||||
"init:sqlite": "node scripts/init-sqlite.js",
|
"init:sqlite": "node scripts/init-sqlite.js",
|
||||||
"init:postgres": "node scripts/init-postgres.js",
|
"init:postgres": "node scripts/init-postgres.js",
|
||||||
|
"init:turso": "node scripts/init-turso.js",
|
||||||
"db:reset": "rm -f .data/moontv.db .data/moontv.db-shm .data/moontv.db-wal && node scripts/init-sqlite.js",
|
"db:reset": "rm -f .data/moontv.db .data/moontv.db-shm .data/moontv.db-wal && node scripts/init-sqlite.js",
|
||||||
"build:edgeone": "pnpm edgeone:build",
|
"build:edgeone": "pnpm edgeone:build",
|
||||||
"preview:edgeone": "BUILD_TARGET=edgeone EDGEONE_PAGES=1 node scripts/edgeone-local-preview.mjs",
|
"preview:edgeone": "BUILD_TARGET=edgeone EDGEONE_PAGES=1 node scripts/edgeone-local-preview.mjs",
|
||||||
@@ -61,6 +62,7 @@
|
|||||||
"he": "^1.2.0",
|
"he": "^1.2.0",
|
||||||
"hls.js": "^1.6.10",
|
"hls.js": "^1.6.10",
|
||||||
"https-proxy-agent": "^7.0.6",
|
"https-proxy-agent": "^7.0.6",
|
||||||
|
"@libsql/client": "^0.15.0",
|
||||||
"lucide-react": "^0.438.0",
|
"lucide-react": "^0.438.0",
|
||||||
"media-icons": "^1.1.5",
|
"media-icons": "^1.1.5",
|
||||||
"mux.js": "^6.3.0",
|
"mux.js": "^6.3.0",
|
||||||
|
|||||||
Generated
+185
-19
@@ -26,6 +26,9 @@ importers:
|
|||||||
'@heroicons/react':
|
'@heroicons/react':
|
||||||
specifier: ^2.2.0
|
specifier: ^2.2.0
|
||||||
version: 2.2.0([email protected])
|
version: 2.2.0([email protected])
|
||||||
|
'@libsql/client':
|
||||||
|
specifier: ^0.15.0
|
||||||
|
version: 0.15.15([email protected])
|
||||||
'@types/crypto-js':
|
'@types/crypto-js':
|
||||||
specifier: ^4.2.2
|
specifier: ^4.2.2
|
||||||
version: 4.2.2
|
version: 4.2.2
|
||||||
@@ -2566,6 +2569,67 @@ packages:
|
|||||||
'@jspm/[email protected]':
|
'@jspm/[email protected]':
|
||||||
resolution: {integrity: sha512-3sRl+pkyFY/kLmHl0cgHiFp2xEqErA8N3ECjMs7serSUBmoJ70lBa0PG5t0IM6WJgdZNyyI0R8YFfi5wM8+mzg==}
|
resolution: {integrity: sha512-3sRl+pkyFY/kLmHl0cgHiFp2xEqErA8N3ECjMs7serSUBmoJ70lBa0PG5t0IM6WJgdZNyyI0R8YFfi5wM8+mzg==}
|
||||||
|
|
||||||
|
'@libsql/[email protected]':
|
||||||
|
resolution: {integrity: sha512-twC0hQxPNHPKfeOv3sNT6u2pturQjLcI+CnpTM0SjRpocEGgfiZ7DWKXLNnsothjyJmDqEsBQJ5ztq9Wlu470w==}
|
||||||
|
|
||||||
|
'@libsql/[email protected]':
|
||||||
|
resolution: {integrity: sha512-C88Z6UKl+OyuKKPwz224riz02ih/zHYI3Ho/LAcVOgjsunIRZoBw7fjRfaH9oPMmSNeQfhGklSG2il1URoOIsA==}
|
||||||
|
|
||||||
|
'@libsql/[email protected]':
|
||||||
|
resolution: {integrity: sha512-K+2RIB1OGFPYQbfay48GakLhqf3ArcbHqPFu7EZiaUcRgFcdw8RoltsMyvbj5ix2fY0HV3Q3Ioa/ByvQdaSM0A==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@libsql/[email protected]':
|
||||||
|
resolution: {integrity: sha512-OtT+KFHsKFy1R5FVadr8FJ2Bb1mghtXTyJkxv0trocq7NuHntSki1eUbxpO5ezJesDvBlqFjnWaYYY516QNLhQ==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@libsql/[email protected]':
|
||||||
|
resolution: {integrity: sha512-OF8fFQSkbL7vJY9rfuegK1R7sPgQ6kFMkDamiEccNUvieQ+3urzfDFI616oPl8V7T9zRmnTkSjMOImYCAVRVuw==}
|
||||||
|
|
||||||
|
'@libsql/[email protected]':
|
||||||
|
resolution: {integrity: sha512-6kK3SUK5Uu56zPq/Las620n5aS9xJq+jMBcNSOmjhNf/MUvdyji4vrMTqD7ptY7/4/CAVEAYDeotUz60LNQHtw==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
|
'@libsql/[email protected]':
|
||||||
|
resolution: {integrity: sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg==}
|
||||||
|
|
||||||
|
'@libsql/[email protected]':
|
||||||
|
resolution: {integrity: sha512-CD4n4zj7SJTHso4nf5cuMoWoMSS7asn5hHygsDuhRl8jjjCTT3yE+xdUvI4J7zsyb53VO5ISh4cwwOtf6k2UhQ==}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@libsql/[email protected]':
|
||||||
|
resolution: {integrity: sha512-2Z9qBVpEJV7OeflzIR3+l5yAd4uTOLxklScYTwpZnkm2vDSGlC1PRlueLaufc4EFITkLKXK2MWBpexuNJfMVcg==}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@libsql/[email protected]':
|
||||||
|
resolution: {integrity: sha512-gURBqaiXIGGwFNEaUj8Ldk7Hps4STtG+31aEidCk5evMMdtsdfL3HPCpvys+ZF/tkOs2MWlRWoSq7SOuCE9k3w==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@libsql/[email protected]':
|
||||||
|
resolution: {integrity: sha512-fwgYZ0H8mUkyVqXZHF3mT/92iIh1N94Owi/f66cPVNsk9BdGKq5gVpoKO+7UxaNzuEH1roJp2QEwsCZMvBLpqg==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@libsql/[email protected]':
|
||||||
|
resolution: {integrity: sha512-y14V0vY0nmMC6G0pHeJcEarcnGU2H6cm21ZceRkacWHvQAEhAG0latQkCtoS2njFOXiYIg+JYPfAoWKbi82rkg==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@libsql/[email protected]':
|
||||||
|
resolution: {integrity: sha512-gquqwA/39tH4pFl+J9n3SOMSymjX+6kZ3kWgY3b94nXFTwac9bnFNMffIomgvlFaC4ArVqMnOZD3nuJ3H3VO1w==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@libsql/[email protected]':
|
||||||
|
resolution: {integrity: sha512-4/0CvEdhi6+KjMxMaVbFM2n2Z44escBRoEYpR+gZg64DdetzGnYm8mcNLcoySaDJZNaBd6wz5DNdgRmcI4hXcg==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
'@mapbox/[email protected]':
|
'@mapbox/[email protected]':
|
||||||
resolution: {integrity: sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==}
|
resolution: {integrity: sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -2580,6 +2644,9 @@ packages:
|
|||||||
'@napi-rs/[email protected]':
|
'@napi-rs/[email protected]':
|
||||||
resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==}
|
resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==}
|
||||||
|
|
||||||
|
'@neon-rs/[email protected]':
|
||||||
|
resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==}
|
||||||
|
|
||||||
'@neondatabase/[email protected]':
|
'@neondatabase/[email protected]':
|
||||||
resolution: {integrity: sha512-siFas6gItqv6wD/pZnvdu34wEqgG3nSE6zWZdq5j2DEsa+VvX8i/5HXJOo06qrw5axPXn+lGCxeR+NLaSPIXug==}
|
resolution: {integrity: sha512-siFas6gItqv6wD/pZnvdu34wEqgG3nSE6zWZdq5j2DEsa+VvX8i/5HXJOo06qrw5axPXn+lGCxeR+NLaSPIXug==}
|
||||||
|
|
||||||
@@ -3817,6 +3884,9 @@ packages:
|
|||||||
'@types/[email protected]':
|
'@types/[email protected]':
|
||||||
resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==}
|
resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==}
|
||||||
|
|
||||||
|
'@types/[email protected]':
|
||||||
|
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
|
||||||
|
|
||||||
'@types/[email protected]':
|
'@types/[email protected]':
|
||||||
resolution: {integrity: sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==}
|
resolution: {integrity: sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==}
|
||||||
|
|
||||||
@@ -5485,6 +5555,10 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==}
|
resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -7180,6 +7254,9 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==}
|
resolution: {integrity: sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-5xVjhUZlHHeuO2W7w2rDFj/Kl1xLX+HjZxdOQwCsUOifl6UaoH1o1wsbsTMz+r0aeC7gCijvru02j6TfKZWzKg==}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==}
|
resolution: {integrity: sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@@ -7333,6 +7410,11 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-r9kw4OA6oDO4dPXkOrXTkArQAafIKAU71hChInV4FxZ69dxCfbwQGDPzqR5/vea94wU705/3AZroEbSoeVWrQw==}
|
resolution: {integrity: sha512-r9kw4OA6oDO4dPXkOrXTkArQAafIKAU71hChInV4FxZ69dxCfbwQGDPzqR5/vea94wU705/3AZroEbSoeVWrQw==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-8lMP8iMgiBzzoNbAPQ59qdVcj6UaE/Vnm+fiwX4doX4Narook0a4GPKWBEv+CR8a1OwbfkgL18uBfBjWdF0Fzg==}
|
||||||
|
cpu: [x64, arm64, wasm32, arm]
|
||||||
|
os: [darwin, linux, win32]
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
|
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
@@ -8606,6 +8688,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
|
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
|
||||||
engines: {node: '>= 0.6.0'}
|
engines: {node: '>= 0.6.0'}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-V6TbZDJ/ZswevgkDNpGt/YqNCiZP9ASfgU+p83uJE6NrGtvSGoOcHLiDCqkMs2+yg7F5qHdLV8d0aS8O26G/KA==}
|
resolution: {integrity: sha512-V6TbZDJ/ZswevgkDNpGt/YqNCiZP9ASfgU+p83uJE6NrGtvSGoOcHLiDCqkMs2+yg7F5qHdLV8d0aS8O26G/KA==}
|
||||||
|
|
||||||
@@ -9691,10 +9776,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
|
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
[email protected]:
|
|
||||||
resolution: {integrity: sha512-VCn+LMHbd4t6sF3wfU/+HKT63C9OoyrSIf4b+vtWHpt2U7/4InZG467YDNMFMR70DdHjAdpPWmw2lzRdg0Xqqg==}
|
|
||||||
engines: {node: '>=20'}
|
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg==}
|
resolution: {integrity: sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
@@ -13716,6 +13797,68 @@ snapshots:
|
|||||||
|
|
||||||
'@jspm/[email protected]': {}
|
'@jspm/[email protected]': {}
|
||||||
|
|
||||||
|
'@libsql/[email protected]([email protected])':
|
||||||
|
dependencies:
|
||||||
|
'@libsql/core': 0.15.15
|
||||||
|
'@libsql/hrana-client': 0.7.0([email protected])
|
||||||
|
js-base64: 3.8.1
|
||||||
|
libsql: 0.5.29
|
||||||
|
promise-limit: 2.7.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
|
'@libsql/[email protected]':
|
||||||
|
dependencies:
|
||||||
|
js-base64: 3.8.1
|
||||||
|
|
||||||
|
'@libsql/[email protected]':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@libsql/[email protected]':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@libsql/[email protected]([email protected])':
|
||||||
|
dependencies:
|
||||||
|
'@libsql/isomorphic-fetch': 0.3.1
|
||||||
|
'@libsql/isomorphic-ws': 0.1.5([email protected])
|
||||||
|
js-base64: 3.8.1
|
||||||
|
node-fetch: 3.3.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
|
'@libsql/[email protected]': {}
|
||||||
|
|
||||||
|
'@libsql/[email protected]([email protected])':
|
||||||
|
dependencies:
|
||||||
|
'@types/ws': 8.18.1
|
||||||
|
ws: 8.21.0([email protected])
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
|
'@libsql/[email protected]':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@libsql/[email protected]':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@libsql/[email protected]':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@libsql/[email protected]':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@libsql/[email protected]':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@libsql/[email protected]':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@libsql/[email protected]':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@mapbox/[email protected]':
|
'@mapbox/[email protected]':
|
||||||
dependencies:
|
dependencies:
|
||||||
consola: 3.4.2
|
consola: 3.4.2
|
||||||
@@ -13745,6 +13888,8 @@ snapshots:
|
|||||||
'@tybys/wasm-util': 0.10.1
|
'@tybys/wasm-util': 0.10.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@neon-rs/[email protected]': {}
|
||||||
|
|
||||||
'@neondatabase/[email protected]':
|
'@neondatabase/[email protected]':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/pg': 8.11.6
|
'@types/pg': 8.11.6
|
||||||
@@ -15139,11 +15284,11 @@ snapshots:
|
|||||||
'@types/[email protected]':
|
'@types/[email protected]':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/eslint': 9.6.1
|
'@types/eslint': 9.6.1
|
||||||
'@types/estree': 1.0.8
|
'@types/estree': 1.0.9
|
||||||
|
|
||||||
'@types/[email protected]':
|
'@types/[email protected]':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.8
|
'@types/estree': 1.0.9
|
||||||
'@types/json-schema': 7.0.15
|
'@types/json-schema': 7.0.15
|
||||||
|
|
||||||
'@types/[email protected]':
|
'@types/[email protected]':
|
||||||
@@ -15285,6 +15430,10 @@ snapshots:
|
|||||||
|
|
||||||
'@types/[email protected]': {}
|
'@types/[email protected]': {}
|
||||||
|
|
||||||
|
'@types/[email protected]':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 24.0.3
|
||||||
|
|
||||||
'@types/[email protected]':
|
'@types/[email protected]':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 24.0.3
|
'@types/node': 24.0.3
|
||||||
@@ -16053,9 +16202,9 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
acorn: 8.15.0
|
acorn: 8.15.0
|
||||||
|
|
||||||
[email protected]([email protected]5.0):
|
[email protected]([email protected]7.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn: 8.15.0
|
acorn: 8.17.0
|
||||||
|
|
||||||
[email protected]([email protected]):
|
[email protected]([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -16958,7 +17107,7 @@ snapshots:
|
|||||||
dot-prop: 10.1.0
|
dot-prop: 10.1.0
|
||||||
env-paths: 3.0.0
|
env-paths: 3.0.0
|
||||||
json-schema-typed: 8.0.2
|
json-schema-typed: 8.0.2
|
||||||
semver: 7.7.3
|
semver: 7.8.4
|
||||||
uint8array-extras: 1.5.0
|
uint8array-extras: 1.5.0
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
@@ -17226,6 +17375,8 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
@@ -17354,7 +17505,7 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
type-fest: 5.3.1
|
type-fest: 5.7.0
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -18979,7 +19130,7 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
get-east-asian-width: 1.4.0
|
get-east-asian-width: 1.6.0
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
@@ -19620,6 +19771,8 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
@@ -19774,6 +19927,21 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
dependencies:
|
||||||
|
'@neon-rs/load': 0.0.4
|
||||||
|
detect-libc: 2.0.2
|
||||||
|
optionalDependencies:
|
||||||
|
'@libsql/darwin-arm64': 0.5.29
|
||||||
|
'@libsql/darwin-x64': 0.5.29
|
||||||
|
'@libsql/linux-arm-gnueabihf': 0.5.29
|
||||||
|
'@libsql/linux-arm-musleabihf': 0.5.29
|
||||||
|
'@libsql/linux-arm64-gnu': 0.5.29
|
||||||
|
'@libsql/linux-arm64-musl': 0.5.29
|
||||||
|
'@libsql/linux-x64-gnu': 0.5.29
|
||||||
|
'@libsql/linux-x64-musl': 0.5.29
|
||||||
|
'@libsql/win32-x64-msvc': 0.5.29
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
@@ -20996,6 +21164,8 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
@@ -22258,7 +22428,7 @@ snapshots:
|
|||||||
'@tsconfig/node14': 1.0.3
|
'@tsconfig/node14': 1.0.3
|
||||||
'@tsconfig/node16': 1.0.4
|
'@tsconfig/node16': 1.0.4
|
||||||
'@types/node': 24.0.3
|
'@types/node': 24.0.3
|
||||||
acorn: 8.15.0
|
acorn: 8.17.0
|
||||||
acorn-walk: 8.3.4
|
acorn-walk: 8.3.4
|
||||||
arg: 4.1.3
|
arg: 4.1.3
|
||||||
create-require: 1.1.1
|
create-require: 1.1.1
|
||||||
@@ -22322,10 +22492,6 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]:
|
|
||||||
dependencies:
|
|
||||||
tagged-tag: 1.0.0
|
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
tagged-tag: 1.0.0
|
tagged-tag: 1.0.0
|
||||||
@@ -22805,13 +22971,13 @@ snapshots:
|
|||||||
[email protected]([email protected]):
|
[email protected]([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/eslint-scope': 3.7.7
|
'@types/eslint-scope': 3.7.7
|
||||||
'@types/estree': 1.0.8
|
'@types/estree': 1.0.9
|
||||||
'@types/json-schema': 7.0.15
|
'@types/json-schema': 7.0.15
|
||||||
'@webassemblyjs/ast': 1.14.1
|
'@webassemblyjs/ast': 1.14.1
|
||||||
'@webassemblyjs/wasm-edit': 1.14.1
|
'@webassemblyjs/wasm-edit': 1.14.1
|
||||||
'@webassemblyjs/wasm-parser': 1.14.1
|
'@webassemblyjs/wasm-parser': 1.14.1
|
||||||
acorn: 8.15.0
|
acorn: 8.17.0
|
||||||
acorn-import-phases: 1.0.4([email protected]5.0)
|
acorn-import-phases: 1.0.4([email protected]7.0)
|
||||||
browserslist: 4.28.1
|
browserslist: 4.28.1
|
||||||
chrome-trace-event: 1.0.4
|
chrome-trace-event: 1.0.4
|
||||||
enhanced-resolve: 5.18.4
|
enhanced-resolve: 5.18.4
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ const runtimeEnvKeys = [
|
|||||||
'NEXT_PUBLIC_STORAGE_TYPE',
|
'NEXT_PUBLIC_STORAGE_TYPE',
|
||||||
'UPSTASH_URL',
|
'UPSTASH_URL',
|
||||||
'UPSTASH_TOKEN',
|
'UPSTASH_TOKEN',
|
||||||
|
'TURSO_URL',
|
||||||
|
'TURSO_TOKEN',
|
||||||
'TMDB_API_KEY',
|
'TMDB_API_KEY',
|
||||||
'TMDB_IMAGE_DOMAIN',
|
'TMDB_IMAGE_DOMAIN',
|
||||||
'NEXT_PUBLIC_SITE_NAME',
|
'NEXT_PUBLIC_SITE_NAME',
|
||||||
|
|||||||
@@ -0,0 +1,205 @@
|
|||||||
|
/**
|
||||||
|
* Turso (libSQL) 数据库初始化脚本
|
||||||
|
*
|
||||||
|
* 创建数据库表结构并初始化默认管理员用户
|
||||||
|
* 复用 SQLite 迁移文件(Turso 基于 libSQL,完全兼容 SQLite 语法)
|
||||||
|
*
|
||||||
|
* 使用方式:
|
||||||
|
* 1. 安装依赖:pnpm install
|
||||||
|
* 2. 设置环境变量:
|
||||||
|
* export TURSO_URL=libsql://your-database.turso.io
|
||||||
|
* export TURSO_TOKEN=your-auth-token
|
||||||
|
* export USERNAME=admin
|
||||||
|
* export PASSWORD=your-password
|
||||||
|
* 3. 运行:node scripts/init-turso.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { createClient } = require('@libsql/client');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
const MIGRATIONS_DIR = path.join(__dirname, '../migrations');
|
||||||
|
const MIGRATION_BASELINE_CUTOFF = '008_web_push_notifications.sql';
|
||||||
|
|
||||||
|
function hashPassword(password) {
|
||||||
|
return crypto.createHash('sha256').update(password).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitSqlStatements(sql) {
|
||||||
|
const withoutLineComments = sql
|
||||||
|
.split('\n')
|
||||||
|
.filter((line) => !line.trim().startsWith('--'))
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
return withoutLineComments
|
||||||
|
.split(';')
|
||||||
|
.map((statement) => statement.trim())
|
||||||
|
.filter((statement) => statement.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMigrationFiles() {
|
||||||
|
if (!fs.existsSync(MIGRATIONS_DIR)) {
|
||||||
|
throw new Error(`Migrations directory not found: ${MIGRATIONS_DIR}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs
|
||||||
|
.readdirSync(MIGRATIONS_DIR)
|
||||||
|
.filter((file) => file.endsWith('.sql'))
|
||||||
|
.sort((a, b) => a.localeCompare(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isIgnorableMigrationError(error) {
|
||||||
|
const message =
|
||||||
|
error instanceof Error ? error.message : String(error || '');
|
||||||
|
return (
|
||||||
|
(message.includes('table') && message.includes('already exists')) ||
|
||||||
|
(message.includes('index') && message.includes('already exists')) ||
|
||||||
|
message.includes('duplicate column name')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tableExists(client, tableName) {
|
||||||
|
const result = await client.execute({
|
||||||
|
sql: "SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?",
|
||||||
|
args: [tableName],
|
||||||
|
});
|
||||||
|
return result.rows.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureMigrationTable(client) {
|
||||||
|
await client.execute(`
|
||||||
|
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||||
|
filename TEXT PRIMARY KEY,
|
||||||
|
applied_at INTEGER NOT NULL
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAppliedMigrations(client) {
|
||||||
|
const result = await client.execute('SELECT filename FROM schema_migrations');
|
||||||
|
return new Set(result.rows.map((row) => row.filename));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function markMigrationApplied(client, filename) {
|
||||||
|
await client.execute({
|
||||||
|
sql: 'INSERT OR IGNORE INTO schema_migrations (filename, applied_at) VALUES (?, ?)',
|
||||||
|
args: [filename, Date.now()],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function seedExistingMigrationBaseline(
|
||||||
|
client,
|
||||||
|
migrationFiles,
|
||||||
|
hadExistingSchema
|
||||||
|
) {
|
||||||
|
const applied = await getAppliedMigrations(client);
|
||||||
|
if (!hadExistingSchema || applied.size > 0) return;
|
||||||
|
|
||||||
|
for (const file of migrationFiles) {
|
||||||
|
if (file.localeCompare(MIGRATION_BASELINE_CUTOFF) < 0) {
|
||||||
|
await markMigrationApplied(client, file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runMigrations(client) {
|
||||||
|
const migrationFiles = getMigrationFiles();
|
||||||
|
const hadExistingSchema = await tableExists(client, 'users');
|
||||||
|
await ensureMigrationTable(client);
|
||||||
|
await seedExistingMigrationBaseline(client, migrationFiles, hadExistingSchema);
|
||||||
|
|
||||||
|
for (const file of migrationFiles) {
|
||||||
|
const applied = await getAppliedMigrations(client);
|
||||||
|
if (applied.has(file)) {
|
||||||
|
console.log(`⏭️ Migration already applied: ${file}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const migrationPath = path.join(MIGRATIONS_DIR, file);
|
||||||
|
const sql = fs.readFileSync(migrationPath, 'utf8');
|
||||||
|
const statements = splitSqlStatements(sql);
|
||||||
|
|
||||||
|
console.log(`▶️ Applying migration: ${file}`);
|
||||||
|
for (const statement of statements) {
|
||||||
|
try {
|
||||||
|
await client.execute(statement);
|
||||||
|
} catch (error) {
|
||||||
|
if (isIgnorableMigrationError(error)) {
|
||||||
|
console.log(`⏭️ Statement skipped in ${file}: ${error.message}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await markMigrationApplied(client, file);
|
||||||
|
console.log(`✅ Migration applied: ${file}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureDefaultAdmin(client) {
|
||||||
|
const username = process.env.USERNAME || 'admin';
|
||||||
|
const password = process.env.PASSWORD || '123456789';
|
||||||
|
const passwordHash = hashPassword(password);
|
||||||
|
|
||||||
|
const existingUser = await client.execute({
|
||||||
|
sql: 'SELECT username FROM users WHERE username = ? LIMIT 1',
|
||||||
|
args: [username],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingUser.rows.length > 0) {
|
||||||
|
console.log(`ℹ️ Admin user already exists: ${username}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.execute({
|
||||||
|
sql: `INSERT INTO users (
|
||||||
|
username, password_hash, role, created_at,
|
||||||
|
playrecord_migrated, favorite_migrated, skip_migrated
|
||||||
|
) VALUES (?, ?, 'owner', ?, 1, 1, 1)`,
|
||||||
|
args: [username, passwordHash, Date.now()],
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✅ Default admin user created: ${username}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initTursoDatabase() {
|
||||||
|
const tursoUrl = process.env.TURSO_URL;
|
||||||
|
const tursoToken = process.env.TURSO_TOKEN;
|
||||||
|
|
||||||
|
if (!tursoUrl || !tursoToken) {
|
||||||
|
console.error(
|
||||||
|
'❌ TURSO_URL and TURSO_TOKEN environment variables must be set'
|
||||||
|
);
|
||||||
|
console.error('');
|
||||||
|
console.error('Example:');
|
||||||
|
console.error(' export TURSO_URL=libsql://your-database.turso.io');
|
||||||
|
console.error(' export TURSO_TOKEN=your-auth-token');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = createClient({
|
||||||
|
url: tursoUrl,
|
||||||
|
authToken: tursoToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('📦 Initializing Turso (libSQL) database...');
|
||||||
|
console.log('🔗 Database URL:', tursoUrl);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await runMigrations(client);
|
||||||
|
await ensureDefaultAdmin(client);
|
||||||
|
console.log('');
|
||||||
|
console.log('🎉 Turso database is ready!');
|
||||||
|
console.log('');
|
||||||
|
console.log('Next steps:');
|
||||||
|
console.log('1. Set NEXT_PUBLIC_STORAGE_TYPE=turso in your environment');
|
||||||
|
console.log('2. Set TURSO_URL and TURSO_TOKEN environment variables');
|
||||||
|
console.log('3. Deploy your application');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('❌ Turso initialization failed:', err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initTursoDatabase();
|
||||||
+34
-1
@@ -17,7 +17,7 @@ import {
|
|||||||
SkipConfig,
|
SkipConfig,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
// storage type 常量: 'localstorage' | 'redis' | 'upstash' | 'kvrocks' | 'd1' | 'postgres',默认 'localstorage'
|
// storage type 常量: 'localstorage' | 'redis' | 'upstash' | 'kvrocks' | 'd1' | 'postgres' | 'turso',默认 'localstorage'
|
||||||
const IS_CLOUDFLARE_BUILD =
|
const IS_CLOUDFLARE_BUILD =
|
||||||
process.env.CF_PAGES === '1' || process.env.BUILD_TARGET === 'cloudflare';
|
process.env.CF_PAGES === '1' || process.env.BUILD_TARGET === 'cloudflare';
|
||||||
const STORAGE_TYPE =
|
const STORAGE_TYPE =
|
||||||
@@ -28,6 +28,7 @@ const STORAGE_TYPE =
|
|||||||
| 'kvrocks'
|
| 'kvrocks'
|
||||||
| 'd1'
|
| 'd1'
|
||||||
| 'postgres'
|
| 'postgres'
|
||||||
|
| 'turso'
|
||||||
| undefined) || 'localstorage';
|
| undefined) || 'localstorage';
|
||||||
|
|
||||||
// 创建存储实例
|
// 创建存储实例
|
||||||
@@ -70,6 +71,15 @@ function createStorage(): IStorage {
|
|||||||
// 动态导入 PostgresStorage 以避免客户端打包
|
// 动态导入 PostgresStorage 以避免客户端打包
|
||||||
const { PostgresStorage } = require('./postgres.db');
|
const { PostgresStorage } = require('./postgres.db');
|
||||||
return new PostgresStorage(postgresAdapter);
|
return new PostgresStorage(postgresAdapter);
|
||||||
|
case 'turso':
|
||||||
|
// TursoStorage 只能在服务端使用,客户端会报错
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
throw new Error('TursoStorage can only be used on the server side');
|
||||||
|
}
|
||||||
|
const tursoAdapter = getTursoAdapter();
|
||||||
|
// 复用 D1Storage(Turso 基于 libSQL/SQLite,SQL 语法完全兼容)
|
||||||
|
const { D1Storage: TursoD1Storage } = require('./d1.db');
|
||||||
|
return new TursoD1Storage(tursoAdapter);
|
||||||
case 'localstorage':
|
case 'localstorage':
|
||||||
default:
|
default:
|
||||||
return null as unknown as IStorage;
|
return null as unknown as IStorage;
|
||||||
@@ -89,6 +99,29 @@ function getPostgresAdapter(): any {
|
|||||||
return new PostgresAdapter();
|
return new PostgresAdapter();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Turso 适配器
|
||||||
|
* 使用 @libsql/client 连接 Turso (libSQL) 远程数据库
|
||||||
|
* 适用于 EdgeOne Pages 等无内置数据库的边缘平台
|
||||||
|
*/
|
||||||
|
function getTursoAdapter(): any {
|
||||||
|
// 动态导入适配器以避免客户端打包
|
||||||
|
const { TursoAdapter } = require('./turso-adapter');
|
||||||
|
|
||||||
|
const tursoUrl = process.env.TURSO_URL;
|
||||||
|
const tursoToken = process.env.TURSO_TOKEN;
|
||||||
|
|
||||||
|
if (!tursoUrl || !tursoToken) {
|
||||||
|
throw new Error(
|
||||||
|
'TURSO_URL and TURSO_TOKEN env variables must be set for Turso storage'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Using Turso (libSQL) database');
|
||||||
|
|
||||||
|
return new TursoAdapter(tursoUrl, tursoToken);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取 D1 适配器
|
* 获取 D1 适配器
|
||||||
* 开发环境:使用 better-sqlite3
|
* 开发环境:使用 better-sqlite3
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turso (libSQL) 适配器
|
||||||
|
*
|
||||||
|
* 将 @libsql/client API 转换为与 D1 兼容的接口
|
||||||
|
* Turso 基于 libSQL(SQLite 开源分支),SQL 语法与 D1/SQLite 完全兼容
|
||||||
|
*
|
||||||
|
* 适用于 EdgeOne Pages 等无内置数据库的边缘平台
|
||||||
|
*
|
||||||
|
* 注意:此模块仅在服务端使用,通过 webpack 配置排除客户端打包
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createClient, type Client } from '@libsql/client';
|
||||||
|
import { DatabaseAdapter, D1PreparedStatement, D1Result } from './d1-adapter';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turso 适配器
|
||||||
|
*
|
||||||
|
* 使用 @libsql/client 包装为 D1 兼容接口
|
||||||
|
*/
|
||||||
|
export class TursoAdapter implements DatabaseAdapter {
|
||||||
|
private client: Client;
|
||||||
|
|
||||||
|
constructor(url: string, authToken: string) {
|
||||||
|
this.client = createClient({
|
||||||
|
url,
|
||||||
|
authToken,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare(query: string): D1PreparedStatement {
|
||||||
|
return new TursoPreparedStatement(this.client, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
async batch(statements: D1PreparedStatement[]): Promise<D1Result[]> {
|
||||||
|
// Turso/libSQL 原生支持 batch
|
||||||
|
const libsqlStatements = statements.map(
|
||||||
|
(stmt) => (stmt as TursoPreparedStatement).toLibSQLBatch()
|
||||||
|
);
|
||||||
|
const results = await this.client.batch(libsqlStatements, 'write');
|
||||||
|
return results.map((result) => ({
|
||||||
|
success: true,
|
||||||
|
results: result.rows || [],
|
||||||
|
meta: {
|
||||||
|
changes: result.rowsAffected,
|
||||||
|
last_row_id:
|
||||||
|
result.lastInsertRowid !== undefined
|
||||||
|
? Number(result.lastInsertRowid)
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async exec(query: string): Promise<void> {
|
||||||
|
await this.client.executeMultiple(query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turso PreparedStatement 包装器
|
||||||
|
* 将 @libsql/client API 转换为 D1 兼容 API
|
||||||
|
*/
|
||||||
|
class TursoPreparedStatement implements D1PreparedStatement {
|
||||||
|
private params: any[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private client: Client,
|
||||||
|
private query: string
|
||||||
|
) {}
|
||||||
|
|
||||||
|
bind(...values: any[]): D1PreparedStatement {
|
||||||
|
this.params = values;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行查询并返回第一行
|
||||||
|
*/
|
||||||
|
async first<T = any>(colName?: string): Promise<T | null> {
|
||||||
|
try {
|
||||||
|
const result = await this.client.execute({
|
||||||
|
sql: this.query,
|
||||||
|
args: this.params,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.rows || result.rows.length === 0) return null;
|
||||||
|
|
||||||
|
const row = result.rows[0];
|
||||||
|
if (colName) return (row as any)[colName] ?? null;
|
||||||
|
|
||||||
|
return row as T;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Turso first() error:', err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行查询并返回结果
|
||||||
|
*/
|
||||||
|
async run<T = any>(): Promise<D1Result<T>> {
|
||||||
|
try {
|
||||||
|
const result = await this.client.execute({
|
||||||
|
sql: this.query,
|
||||||
|
args: this.params,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
meta: {
|
||||||
|
changes: result.rowsAffected,
|
||||||
|
last_row_id:
|
||||||
|
result.lastInsertRowid !== undefined
|
||||||
|
? Number(result.lastInsertRowid)
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
results: result.rows as T[],
|
||||||
|
};
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Turso run() error:', err);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: err.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行查询并返回所有行
|
||||||
|
*/
|
||||||
|
async all<T = any>(): Promise<D1Result<T>> {
|
||||||
|
try {
|
||||||
|
const result = await this.client.execute({
|
||||||
|
sql: this.query,
|
||||||
|
args: this.params,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
results: (result.rows || []) as T[],
|
||||||
|
};
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Turso all() error:', err);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: err.message,
|
||||||
|
results: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换为 libSQL batch 格式
|
||||||
|
*/
|
||||||
|
toLibSQLBatch(): { sql: string; args: any[] } {
|
||||||
|
return { sql: this.query, args: this.params };
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user