mirror of
https://github.com/saltbo/zpan.git
synced 2026-08-29 00:01:42 +08:00
feat: v2.5.0 T1 — libSQL (Turso) platform adapter + Docker Turso opt-in (#326)
* feat: add libSQL (Turso) platform adapter and Docker opt-in - server/platform/libsql.ts: createLibsqlPlatform() using @libsql/client + drizzle-orm/libsql; accepts plain env record; async migrate at boot; authToken optional for file:// URLs - server/entry-node.ts: select platform at startup — libsql when TURSO_DATABASE_URL is set, otherwise existing SQLite via createNodePlatform() - drizzle.config.ts: switch to turso dialect when TURSO_DATABASE_URL is set - vitest.libsql.config.ts + server/platform/libsql.libsql-test.ts: smoke suite covering connect, migrations, insert/select against users + storages tables - package.json: add @libsql/client dependency; add test:libsql script; externalize @libsql/client in build:node tsup command - vitest.config.ts: exclude *.libsql-test.ts from coverage - docs/deploy/docker.md: document Turso opt-in with copy-pasteable docker-compose snippet - CONTRIBUTING.md: add Turso migrate path paragraph under Database Migrations Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f * refactor: turn bootstrap.ts into a Platform-accepting factory - server/bootstrap.ts: replace singleton module-scope script with exportable createBootstrap(platform) async factory; reads BETTER_AUTH_SECRET/BETTER_AUTH_URL/TRUSTED_ORIGINS from platform.getEnv so every future entry (Lambda, Vercel, Netlify, Azure) can reuse it - server/entry-node.ts: slim down to platform selection + createBootstrap call; no more duplicate auth/app wiring - server/dev.ts: thin vite-dev-server entry that creates NodePlatform and calls createBootstrap; replaces the former default export in bootstrap.ts - vite.config.ts: update node dev server entry to server/dev.ts - server/platform/libsql.ts: fix getEnv to check env record before falling back to process.env, matching the cloudflare.ts pattern Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f * style: apply biome auto-fixes for pre-existing lint issues Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f --------- Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
This commit is contained in:
@@ -93,6 +93,24 @@ npm run db:reset:d1 # Reset D1 local database (.wrangler)
|
||||
|
||||
Migration files live in `migrations/` at project root. Always commit them.
|
||||
|
||||
### Turso (libSQL) migrate path
|
||||
|
||||
When deploying the Node/Docker image against a Turso (libSQL) database, set `TURSO_DATABASE_URL` (and `TURSO_AUTH_TOKEN` for remote URLs) before running `db:migrate`. `drizzle.config.ts` detects the env var and switches to the `turso` dialect automatically:
|
||||
|
||||
```sh
|
||||
TURSO_DATABASE_URL=libsql://your-db.turso.io \
|
||||
TURSO_AUTH_TOKEN=your-token \
|
||||
npm run db:migrate
|
||||
```
|
||||
|
||||
For local libSQL files the token can be omitted:
|
||||
|
||||
```sh
|
||||
TURSO_DATABASE_URL=file:./zpan.db npm run db:migrate
|
||||
```
|
||||
|
||||
Migrations run automatically at Docker container startup when `TURSO_DATABASE_URL` is set. See [docs/deploy/docker.md](docs/deploy/docker.md) for the full Docker + Turso setup.
|
||||
|
||||
## Deployment
|
||||
|
||||
Primary target is Cloudflare Workers. Node.js (Docker) is the backup runtime.
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# Docker Deployment
|
||||
|
||||
ZPan ships as a single Docker image. By default it uses an embedded SQLite database (`better-sqlite3`). For production multi-replica deployments you can opt into [Turso](https://turso.tech) (libSQL) as a shared remote database.
|
||||
|
||||
## Default: local SQLite
|
||||
|
||||
No extra configuration needed. Mount a volume so the database survives container restarts:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
zpan:
|
||||
image: ghcr.io/saltbo/zpan:latest
|
||||
ports:
|
||||
- "8222:8222"
|
||||
environment:
|
||||
PORT: 8222
|
||||
BETTER_AUTH_SECRET: <generate with: openssl rand -base64 32>
|
||||
BETTER_AUTH_URL: https://your-domain.example
|
||||
DATABASE_URL: /data/zpan.db
|
||||
volumes:
|
||||
- zpan-data:/data
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
zpan-data:
|
||||
```
|
||||
|
||||
Migrations run automatically at startup.
|
||||
|
||||
## Turso (libSQL) opt-in
|
||||
|
||||
Set `TURSO_DATABASE_URL` to switch from local SQLite to a Turso (or self-hosted libSQL) database. `TURSO_AUTH_TOKEN` is required for remote URLs; it can be omitted for local `file://` URLs.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
zpan:
|
||||
image: ghcr.io/saltbo/zpan:latest
|
||||
ports:
|
||||
- "8222:8222"
|
||||
environment:
|
||||
PORT: 8222
|
||||
BETTER_AUTH_SECRET: <generate with: openssl rand -base64 32>
|
||||
BETTER_AUTH_URL: https://your-domain.example
|
||||
TURSO_DATABASE_URL: libsql://your-db-name-orgname.turso.io
|
||||
TURSO_AUTH_TOKEN: <your-turso-auth-token>
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
When `TURSO_DATABASE_URL` is present:
|
||||
- `DATABASE_URL` is ignored.
|
||||
- Migrations are applied automatically at startup via `drizzle-orm/libsql/migrator`.
|
||||
- `TURSO_AUTH_TOKEN` may be omitted only for `file://` URLs (local libSQL files).
|
||||
|
||||
### Running migrations manually against Turso
|
||||
|
||||
```sh
|
||||
TURSO_DATABASE_URL=libsql://your-db.turso.io \
|
||||
TURSO_AUTH_TOKEN=your-token \
|
||||
npm run db:migrate
|
||||
```
|
||||
|
||||
`drizzle.config.ts` automatically switches to the `turso` dialect when `TURSO_DATABASE_URL` is set, so `npm run db:generate` and `npm run db:migrate` work against Turso without any extra flags.
|
||||
|
||||
### Obtaining a Turso auth token
|
||||
|
||||
```sh
|
||||
turso db tokens create your-db-name
|
||||
```
|
||||
|
||||
Or create one in the [Turso dashboard](https://app.turso.tech).
|
||||
+20
-8
@@ -1,10 +1,22 @@
|
||||
import { defineConfig } from 'drizzle-kit'
|
||||
|
||||
export default defineConfig({
|
||||
schema: ['./server/db/schema.ts', './server/db/auth-schema.ts'],
|
||||
out: './migrations',
|
||||
dialect: 'sqlite',
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL || 'zpan.db',
|
||||
},
|
||||
})
|
||||
const tursoUrl = process.env.TURSO_DATABASE_URL
|
||||
|
||||
export default tursoUrl
|
||||
? defineConfig({
|
||||
schema: ['./server/db/schema.ts', './server/db/auth-schema.ts'],
|
||||
out: './migrations',
|
||||
dialect: 'turso',
|
||||
dbCredentials: {
|
||||
url: tursoUrl,
|
||||
authToken: process.env.TURSO_AUTH_TOKEN,
|
||||
},
|
||||
})
|
||||
: defineConfig({
|
||||
schema: ['./server/db/schema.ts', './server/db/auth-schema.ts'],
|
||||
out: './migrations',
|
||||
dialect: 'sqlite',
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL || 'zpan.db',
|
||||
},
|
||||
})
|
||||
|
||||
Generated
+372
-3
@@ -17,6 +17,7 @@
|
||||
"@hono/node-server": "^1.13.0",
|
||||
"@hono/zod-validator": "^0.7.6",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@libsql/client": "^0.17.2",
|
||||
"@opentelemetry/api": "^1.9.1",
|
||||
"@radix-ui/react-avatar": "^1.1.0",
|
||||
"@radix-ui/react-dialog": "^1.1.0",
|
||||
@@ -3582,6 +3583,167 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@libsql/client": {
|
||||
"version": "0.17.2",
|
||||
"resolved": "https://registry.npmjs.org/@libsql/client/-/client-0.17.2.tgz",
|
||||
"integrity": "sha512-0aw0S3iQMHvOxfRt5j1atoCCPMT3gjsB2PS8/uxSM1DcDn39xqz6RlgSMxtP8I3JsxIXAFuw7S41baLEw0Zi+Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@libsql/core": "^0.17.2",
|
||||
"@libsql/hrana-client": "^0.9.0",
|
||||
"js-base64": "^3.7.5",
|
||||
"libsql": "^0.5.28",
|
||||
"promise-limit": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@libsql/core": {
|
||||
"version": "0.17.2",
|
||||
"resolved": "https://registry.npmjs.org/@libsql/core/-/core-0.17.2.tgz",
|
||||
"integrity": "sha512-L8qv12HZ/jRBcETVR3rscP0uHNxh+K3EABSde6scCw7zfOdiLqO3MAkJaeE1WovPsjXzsN/JBoZED4+7EZVT3g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-base64": "^3.7.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@libsql/darwin-arm64": {
|
||||
"version": "0.5.29",
|
||||
"resolved": "https://registry.npmjs.org/@libsql/darwin-arm64/-/darwin-arm64-0.5.29.tgz",
|
||||
"integrity": "sha512-K+2RIB1OGFPYQbfay48GakLhqf3ArcbHqPFu7EZiaUcRgFcdw8RoltsMyvbj5ix2fY0HV3Q3Ioa/ByvQdaSM0A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@libsql/darwin-x64": {
|
||||
"version": "0.5.29",
|
||||
"resolved": "https://registry.npmjs.org/@libsql/darwin-x64/-/darwin-x64-0.5.29.tgz",
|
||||
"integrity": "sha512-OtT+KFHsKFy1R5FVadr8FJ2Bb1mghtXTyJkxv0trocq7NuHntSki1eUbxpO5ezJesDvBlqFjnWaYYY516QNLhQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@libsql/hrana-client": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@libsql/hrana-client/-/hrana-client-0.9.0.tgz",
|
||||
"integrity": "sha512-pxQ1986AuWfPX4oXzBvLwBnfgKDE5OMhAdR/5cZmRaB4Ygz5MecQybvwZupnRz341r2CtFmbk/BhSu7k2Lm+Jw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@libsql/isomorphic-ws": "^0.1.5",
|
||||
"cross-fetch": "^4.0.0",
|
||||
"js-base64": "^3.7.5",
|
||||
"node-fetch": "^3.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@libsql/isomorphic-ws": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@libsql/isomorphic-ws/-/isomorphic-ws-0.1.5.tgz",
|
||||
"integrity": "sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/ws": "^8.5.4",
|
||||
"ws": "^8.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@libsql/linux-arm-gnueabihf": {
|
||||
"version": "0.5.29",
|
||||
"resolved": "https://registry.npmjs.org/@libsql/linux-arm-gnueabihf/-/linux-arm-gnueabihf-0.5.29.tgz",
|
||||
"integrity": "sha512-CD4n4zj7SJTHso4nf5cuMoWoMSS7asn5hHygsDuhRl8jjjCTT3yE+xdUvI4J7zsyb53VO5ISh4cwwOtf6k2UhQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@libsql/linux-arm-musleabihf": {
|
||||
"version": "0.5.29",
|
||||
"resolved": "https://registry.npmjs.org/@libsql/linux-arm-musleabihf/-/linux-arm-musleabihf-0.5.29.tgz",
|
||||
"integrity": "sha512-2Z9qBVpEJV7OeflzIR3+l5yAd4uTOLxklScYTwpZnkm2vDSGlC1PRlueLaufc4EFITkLKXK2MWBpexuNJfMVcg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@libsql/linux-arm64-gnu": {
|
||||
"version": "0.5.29",
|
||||
"resolved": "https://registry.npmjs.org/@libsql/linux-arm64-gnu/-/linux-arm64-gnu-0.5.29.tgz",
|
||||
"integrity": "sha512-gURBqaiXIGGwFNEaUj8Ldk7Hps4STtG+31aEidCk5evMMdtsdfL3HPCpvys+ZF/tkOs2MWlRWoSq7SOuCE9k3w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@libsql/linux-arm64-musl": {
|
||||
"version": "0.5.29",
|
||||
"resolved": "https://registry.npmjs.org/@libsql/linux-arm64-musl/-/linux-arm64-musl-0.5.29.tgz",
|
||||
"integrity": "sha512-fwgYZ0H8mUkyVqXZHF3mT/92iIh1N94Owi/f66cPVNsk9BdGKq5gVpoKO+7UxaNzuEH1roJp2QEwsCZMvBLpqg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@libsql/linux-x64-gnu": {
|
||||
"version": "0.5.29",
|
||||
"resolved": "https://registry.npmjs.org/@libsql/linux-x64-gnu/-/linux-x64-gnu-0.5.29.tgz",
|
||||
"integrity": "sha512-y14V0vY0nmMC6G0pHeJcEarcnGU2H6cm21ZceRkacWHvQAEhAG0latQkCtoS2njFOXiYIg+JYPfAoWKbi82rkg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@libsql/linux-x64-musl": {
|
||||
"version": "0.5.29",
|
||||
"resolved": "https://registry.npmjs.org/@libsql/linux-x64-musl/-/linux-x64-musl-0.5.29.tgz",
|
||||
"integrity": "sha512-gquqwA/39tH4pFl+J9n3SOMSymjX+6kZ3kWgY3b94nXFTwac9bnFNMffIomgvlFaC4ArVqMnOZD3nuJ3H3VO1w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@libsql/win32-x64-msvc": {
|
||||
"version": "0.5.29",
|
||||
"resolved": "https://registry.npmjs.org/@libsql/win32-x64-msvc/-/win32-x64-msvc-0.5.29.tgz",
|
||||
"integrity": "sha512-4/0CvEdhi6+KjMxMaVbFM2n2Z44escBRoEYpR+gZg64DdetzGnYm8mcNLcoySaDJZNaBd6wz5DNdgRmcI4hXcg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@napi-rs/canvas": {
|
||||
"version": "0.1.97",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.97.tgz",
|
||||
@@ -3832,6 +3994,12 @@
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@neon-rs/load": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@neon-rs/load/-/load-0.0.4.tgz",
|
||||
"integrity": "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@noble/ciphers": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.1.1.tgz",
|
||||
@@ -9609,7 +9777,6 @@
|
||||
"version": "25.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
|
||||
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.19.0"
|
||||
@@ -9651,6 +9818,15 @@
|
||||
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@ungap/structured-clone": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
|
||||
@@ -10616,6 +10792,57 @@
|
||||
"integrity": "sha512-aVf4A4hI2w70LnF7GG+7xDQUkliwiXWXFvTjkip4+b64ygDQ2sJPRSKFDHbxn8o0xu9QzPkMuuiWIXyFSE2slA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-fetch": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz",
|
||||
"integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-fetch": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-fetch/node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cross-fetch/node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-fetch/node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/cross-fetch/node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/css-tree": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
|
||||
@@ -10637,6 +10864,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/data-uri-to-buffer": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
||||
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/data-urls": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
|
||||
@@ -11653,6 +11889,29 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fetch-blob": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
||||
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-domexception": "^1.0.0",
|
||||
"web-streams-polyfill": "^3.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20 || >= 14.13"
|
||||
}
|
||||
},
|
||||
"node_modules/file-selector": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz",
|
||||
@@ -11696,6 +11955,18 @@
|
||||
"rollup": "^4.34.8"
|
||||
}
|
||||
},
|
||||
"node_modules/formdata-polyfill": {
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fetch-blob": "^3.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-constants": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||
@@ -12228,6 +12499,12 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/js-base64": {
|
||||
"version": "3.7.8",
|
||||
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz",
|
||||
"integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -12340,6 +12617,47 @@
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/libsql": {
|
||||
"version": "0.5.29",
|
||||
"resolved": "https://registry.npmjs.org/libsql/-/libsql-0.5.29.tgz",
|
||||
"integrity": "sha512-8lMP8iMgiBzzoNbAPQ59qdVcj6UaE/Vnm+fiwX4doX4Narook0a4GPKWBEv+CR8a1OwbfkgL18uBfBjWdF0Fzg==",
|
||||
"cpu": [
|
||||
"x64",
|
||||
"arm64",
|
||||
"wasm32",
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"os": [
|
||||
"darwin",
|
||||
"linux",
|
||||
"win32"
|
||||
],
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"node_modules/libsql/node_modules/detect-libc": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz",
|
||||
"integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||
@@ -13913,6 +14231,44 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
||||
"deprecated": "Use your platform's native DOMException instead",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
|
||||
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"data-uri-to-buffer": "^4.0.0",
|
||||
"fetch-blob": "^3.1.4",
|
||||
"formdata-polyfill": "^4.0.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/node-fetch"
|
||||
}
|
||||
},
|
||||
"node_modules/node-readable-to-web-readable-stream": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/node-readable-to-web-readable-stream/-/node-readable-to-web-readable-stream-0.4.2.tgz",
|
||||
@@ -14343,6 +14699,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/promise-limit": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/promise-limit/-/promise-limit-2.7.0.tgz",
|
||||
"integrity": "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
@@ -15960,7 +16322,6 @@
|
||||
"version": "7.19.2",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
|
||||
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unenv": {
|
||||
@@ -16428,6 +16789,15 @@
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/web-streams-polyfill": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
||||
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
|
||||
@@ -16609,7 +16979,6 @@
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
|
||||
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
|
||||
+4
-2
@@ -7,7 +7,7 @@
|
||||
"dev": "CLOUDFLARE_ENV=staging vite dev",
|
||||
"dev:node": "node --env-file=.dev.vars node_modules/vite/bin/vite.js dev --mode node",
|
||||
"build": "[ \"$WORKERS_CI\" = \"1\" ] && [ \"$WORKERS_CI_BRANCH\" != \"master\" ] && export CLOUDFLARE_ENV=staging; vite build",
|
||||
"build:node": "vite build --mode node && tsup server/entry-node.ts --format esm --outDir dist-server --external better-sqlite3",
|
||||
"build:node": "vite build --mode node && tsup server/entry-node.ts --format esm --outDir dist-server --external better-sqlite3 --external @libsql/client",
|
||||
"deploy": "npm run db:migrate:d1:prod && wrangler deploy",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "drizzle-kit migrate",
|
||||
@@ -19,6 +19,7 @@
|
||||
"typecheck": "tsc --noEmit -p tsconfig.server.json && tsc --noEmit -p tsconfig.web.json",
|
||||
"test": "vitest run",
|
||||
"test:cf": "vitest run --config vitest.cloudflare.config.ts",
|
||||
"test:libsql": "vitest run --config vitest.libsql.config.ts",
|
||||
"test:watch": "vitest",
|
||||
"lint": "biome check .",
|
||||
"lint:fix": "biome check --write .",
|
||||
@@ -38,8 +39,8 @@
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@hono/node-server": "^1.13.0",
|
||||
"@hono/zod-validator": "^0.7.6",
|
||||
"better-sqlite3": "^11.0.0",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@libsql/client": "^0.17.2",
|
||||
"@opentelemetry/api": "^1.9.1",
|
||||
"@radix-ui/react-avatar": "^1.1.0",
|
||||
"@radix-ui/react-dialog": "^1.1.0",
|
||||
@@ -53,6 +54,7 @@
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@vidstack/react": "^1.12.13",
|
||||
"better-auth": "^1.6.2",
|
||||
"better-sqlite3": "^11.0.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
"drizzle-orm": "^0.45.2",
|
||||
|
||||
+16
-12
@@ -1,16 +1,20 @@
|
||||
import { createApp } from './app'
|
||||
import { createAuth } from './auth'
|
||||
import { createNodePlatform } from './platform/node'
|
||||
import type { Platform } from './platform/interface'
|
||||
|
||||
const platform = createNodePlatform()
|
||||
const secret = process.env.BETTER_AUTH_SECRET
|
||||
if (!secret) {
|
||||
throw new Error('BETTER_AUTH_SECRET is required. Set it in the environment before starting the server.')
|
||||
export async function createBootstrap(platform: Platform) {
|
||||
const secret = platform.getEnv('BETTER_AUTH_SECRET')
|
||||
if (!secret) {
|
||||
throw new Error('BETTER_AUTH_SECRET is required. Set it in the environment before starting the server.')
|
||||
}
|
||||
|
||||
const baseURL = platform.getEnv('BETTER_AUTH_URL') || 'http://localhost:5173'
|
||||
const trustedOrigins = platform
|
||||
.getEnv('TRUSTED_ORIGINS')
|
||||
?.split(',')
|
||||
.map((o) => o.trim())
|
||||
.filter(Boolean) || ['http://localhost:5173']
|
||||
|
||||
const auth = await createAuth(platform.db, secret, baseURL, trustedOrigins)
|
||||
return createApp(platform, auth)
|
||||
}
|
||||
const baseURL = process.env.BETTER_AUTH_URL || 'http://localhost:5173'
|
||||
const trustedOrigins = process.env.TRUSTED_ORIGINS?.split(',')
|
||||
.map((o) => o.trim())
|
||||
.filter(Boolean) || ['http://localhost:5173']
|
||||
const auth = await createAuth(platform.db, secret, baseURL, trustedOrigins)
|
||||
|
||||
export default createApp(platform, auth)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createBootstrap } from './bootstrap'
|
||||
import { createNodePlatform } from './platform/node'
|
||||
|
||||
const platform = createNodePlatform()
|
||||
export default await createBootstrap(platform)
|
||||
+12
-1
@@ -1,7 +1,18 @@
|
||||
import { serve } from '@hono/node-server'
|
||||
import { serveStatic } from '@hono/node-server/serve-static'
|
||||
import { Hono } from 'hono'
|
||||
import app from './bootstrap'
|
||||
import { createBootstrap } from './bootstrap'
|
||||
import { createLibsqlPlatform } from './platform/libsql'
|
||||
import { createNodePlatform } from './platform/node'
|
||||
|
||||
const platform = process.env.TURSO_DATABASE_URL
|
||||
? await createLibsqlPlatform({
|
||||
TURSO_DATABASE_URL: process.env.TURSO_DATABASE_URL,
|
||||
TURSO_AUTH_TOKEN: process.env.TURSO_AUTH_TOKEN,
|
||||
})
|
||||
: createNodePlatform()
|
||||
|
||||
const app = await createBootstrap(platform)
|
||||
|
||||
const server = new Hono()
|
||||
server.route('/', app)
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import path from 'node:path'
|
||||
import { beforeAll, describe, expect, it } from 'vitest'
|
||||
import type { Platform } from './interface'
|
||||
import { createLibsqlPlatform } from './libsql'
|
||||
|
||||
const migrationsFolder = path.join(__dirname, '../../migrations')
|
||||
|
||||
let platform: Platform
|
||||
|
||||
beforeAll(async () => {
|
||||
process.env.MIGRATIONS_DIR = migrationsFolder
|
||||
platform = await createLibsqlPlatform({
|
||||
TURSO_DATABASE_URL: 'file::memory:',
|
||||
})
|
||||
}, 30_000)
|
||||
|
||||
describe('libsql platform', () => {
|
||||
it('connects and applies all migrations', () => {
|
||||
expect(platform).toBeDefined()
|
||||
expect(platform.db).toBeDefined()
|
||||
})
|
||||
|
||||
it('returns env values via getEnv', () => {
|
||||
process.env.TEST_LIBSQL_KEY = 'hello'
|
||||
expect(platform.getEnv('TEST_LIBSQL_KEY')).toBe('hello')
|
||||
delete process.env.TEST_LIBSQL_KEY
|
||||
})
|
||||
|
||||
it('returns undefined for missing env keys', () => {
|
||||
expect(platform.getEnv('__NONEXISTENT_KEY__')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('can query the storages table', async () => {
|
||||
const rows = await platform.db.query.storages.findMany()
|
||||
expect(Array.isArray(rows)).toBe(true)
|
||||
})
|
||||
|
||||
it('can query the user table', async () => {
|
||||
const rows = await platform.db.query.user.findMany()
|
||||
expect(Array.isArray(rows)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('createLibsqlPlatform with file:// URL', () => {
|
||||
it('boots without TURSO_AUTH_TOKEN', async () => {
|
||||
const p = await createLibsqlPlatform({
|
||||
TURSO_DATABASE_URL: 'file::memory:',
|
||||
})
|
||||
expect(p.db).toBeDefined()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,32 @@
|
||||
import { createClient } from '@libsql/client'
|
||||
import { drizzle } from 'drizzle-orm/libsql'
|
||||
import { migrate } from 'drizzle-orm/libsql/migrator'
|
||||
import * as authSchema from '../db/auth-schema'
|
||||
import * as schema from '../db/schema'
|
||||
import type { Platform } from './interface'
|
||||
|
||||
interface LibsqlEnv {
|
||||
TURSO_DATABASE_URL: string
|
||||
TURSO_AUTH_TOKEN?: string
|
||||
}
|
||||
|
||||
export async function createLibsqlPlatform(env: LibsqlEnv): Promise<Platform> {
|
||||
const migrationsFolder = process.env.MIGRATIONS_DIR || './migrations'
|
||||
const envRecord: Record<string, string | undefined> = { ...env }
|
||||
|
||||
const client = createClient({
|
||||
url: env.TURSO_DATABASE_URL,
|
||||
authToken: env.TURSO_AUTH_TOKEN,
|
||||
})
|
||||
|
||||
const db = drizzle(client, { schema: { ...schema, ...authSchema } })
|
||||
|
||||
await migrate(db, { migrationsFolder })
|
||||
|
||||
return {
|
||||
db,
|
||||
getEnv(key: string) {
|
||||
return envRecord[key] ?? process.env[key]
|
||||
},
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -20,7 +20,7 @@ export default defineConfig(({ mode }) => ({
|
||||
react(),
|
||||
tailwindcss(),
|
||||
...(mode === 'node'
|
||||
? [devServer({ entry: './server/bootstrap.ts', injectClientScript: false, exclude: [/^(?!\/api\/).*/] })]
|
||||
? [devServer({ entry: './server/dev.ts', injectClientScript: false, exclude: [/^(?!\/api\/).*/] })]
|
||||
: [cloudflare()]),
|
||||
],
|
||||
resolve: {
|
||||
|
||||
@@ -23,6 +23,7 @@ const coverageConfig = {
|
||||
'server/**/*.test.ts',
|
||||
'server/**/*.integration.test.ts',
|
||||
'server/**/*.cf-test.ts',
|
||||
'server/**/*.libsql-test.ts',
|
||||
'server/test/**',
|
||||
'server/platform/**',
|
||||
'server/db/**',
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import path from 'node:path'
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
'@shared': path.resolve(__dirname, './shared'),
|
||||
'@server': path.resolve(__dirname, './server'),
|
||||
},
|
||||
},
|
||||
test: {
|
||||
globals: true,
|
||||
testTimeout: 30_000,
|
||||
include: ['server/**/*.libsql-test.ts'],
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user