Skip to content

Commit b26afbf

Browse files
committed
✨ feat: support server db mode with Postgres / Drizzle ORM / tRPC (lobehub#2556)
1 parent 7789340 commit b26afbf

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

80 files changed

+14637
-226
lines changed

.github/workflows/test.yml

+35-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
11
name: Test CI
2+
23
on: [push, pull_request]
4+
35
jobs:
46
test:
57
runs-on: ubuntu-latest
68

9+
services:
10+
postgres:
11+
image: postgres:16
12+
env:
13+
POSTGRES_PASSWORD: postgres
14+
options: >-
15+
--health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
16+
17+
18+
ports:
19+
- 5432:5432
20+
721
steps:
822
- uses: actions/checkout@v4
923

@@ -18,10 +32,27 @@ jobs:
1832
- name: Lint
1933
run: bun run lint
2034

21-
- name: Test and coverage
22-
run: bun run test:coverage
35+
- name: Test Server Coverage
36+
run: bun run test-server:coverage
37+
env:
38+
DATABASE_TEST_URL: postgresql://postgres:postgres@localhost:5432/postgres
39+
DATABASE_DRIVER: node
40+
NEXT_PUBLIC_SERVICE_MODE: server
41+
KEY_VAULTS_SECRET: LA7n9k3JdEcbSgml2sxfw+4TV1AzaaFU5+R176aQz4s=
42+
43+
- name: Upload Server coverage to Codecov
44+
uses: codecov/codecov-action@v4
45+
with:
46+
token: ${{ secrets.CODECOV_TOKEN }}
47+
files: ./coverage/server/lcov.info
48+
flags: server
49+
50+
- name: Test App Coverage
51+
run: bun run test-app:coverage
2352

24-
- name: Upload coverage to Codecov
53+
- name: Upload App Coverage to Codecov
2554
uses: codecov/codecov-action@v4
2655
with:
27-
token: ${{ secrets.CODECOV_TOKEN }} # required
56+
token: ${{ secrets.CODECOV_TOKEN }}
57+
files: ./coverage/app/lcov.info
58+
flags: app

codecov.yml

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
coverage:
2+
status:
3+
project:
4+
default: off
5+
server:
6+
flags:
7+
- server
8+
app:
9+
flags:
10+
- app
11+
patch: off

drizzle.config.ts

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import * as dotenv from 'dotenv';
2+
import type { Config } from 'drizzle-kit';
3+
4+
// Read the .env file if it exists, or a file specified by the
5+
6+
// dotenv_config_path parameter that's passed to Node.js
7+
8+
dotenv.config();
9+
10+
let connectionString = process.env.DATABASE_URL;
11+
12+
if (process.env.NODE_ENV === 'test') {
13+
console.log('current ENV:', process.env.NODE_ENV);
14+
connectionString = process.env.DATABASE_TEST_URL;
15+
}
16+
17+
if (!connectionString)
18+
throw new Error('`DATABASE_URL` or `DATABASE_TEST_URL` not found in environment');
19+
20+
export default {
21+
dbCredentials: {
22+
url: connectionString,
23+
},
24+
dialect: 'postgresql',
25+
out: './src/database/server/migrations',
26+
27+
schema: './src/database/server/schemas/lobechat.ts',
28+
strict: true,
29+
} satisfies Config;

next.config.mjs

+3
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ const nextConfig = {
6060
},
6161
});
6262

63+
// https://github.com/pinojs/pino/issues/688#issuecomment-637763276
64+
config.externals.push('pino-pretty');
65+
6366
return config;
6467
},
6568
};

package.json

+23-3
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,17 @@
2727
"sideEffects": false,
2828
"scripts": {
2929
"build": "next build",
30-
"postbuild": "npm run build-sitemap",
30+
"postbuild": "npm run build-sitemap && npm run build-migrate-db",
31+
"build-migrate-db": "bun run db:migrate",
3132
"build-sitemap": "next-sitemap --config next-sitemap.config.mjs",
3233
"build:analyze": "ANALYZE=true next build",
3334
"build:docker": "DOCKER=true next build && npm run build-sitemap",
35+
"db:generate": "drizzle-kit generate -- dotenv_config_path='.env'",
36+
"db:migrate": "MIGRATION_DB=1 tsx scripts/migrateServerDB/index.ts",
37+
"db:push": "drizzle-kit push -- dotenv_config_path='.env'",
38+
"db:push-test": "NODE_ENV=test drizzle-kit push -- dotenv_config_path='.env'",
39+
"db:studio": "drizzle-kit studio",
40+
"db:z-pull": "drizzle-kit introspect -- dotenv_config_path='.env'",
3441
"dev": "next dev -p 3010",
3542
"dev:clerk-proxy": "ngrok http http://localhost:3011",
3643
"docs:i18n": "lobe-i18n md && npm run workflow:docs && npm run lint:mdx",
@@ -48,8 +55,11 @@
4855
"release": "semantic-release",
4956
"start": "next start",
5057
"stylelint": "stylelint \"src/**/*.{js,jsx,ts,tsx}\" --fix",
51-
"test": "vitest",
52-
"test:coverage": "vitest run --coverage",
58+
"test": "npm run test-app && npm run test-server",
59+
"test-app": "vitest run --config vitest.config.ts",
60+
"test-app:coverage": "vitest run --config vitest.config.ts --coverage",
61+
"test-server": "vitest run --config vitest.server.config.ts",
62+
"test-server:coverage": "vitest run --config vitest.server.config.ts --coverage",
5363
"test:update": "vitest -u",
5464
"type-check": "tsc --noEmit",
5565
"workflow:docs": "tsx scripts/docsWorkflow/index.ts",
@@ -101,6 +111,7 @@
101111
"@lobehub/tts": "^1.24.1",
102112
"@lobehub/ui": "^1.141.2",
103113
"@microsoft/fetch-event-source": "^2.0.1",
114+
"@neondatabase/serverless": "^0.9.3",
104115
"@next/third-parties": "^14.2.3",
105116
"@sentry/nextjs": "^7.116.0",
106117
"@t3-oss/env-nextjs": "^0.10.1",
@@ -119,6 +130,8 @@
119130
"debug": "^4.3.4",
120131
"dexie": "^3.2.7",
121132
"diff": "^5.2.0",
133+
"drizzle-orm": "^0.30.10",
134+
"drizzle-zod": "^0.5.1",
122135
"fast-deep-equal": "^3.1.3",
123136
"gpt-tokenizer": "^2.1.2",
124137
"i18next": "^23.11.5",
@@ -141,6 +154,7 @@
141154
"nuqs": "^1.17.4",
142155
"ollama": "^0.5.1",
143156
"openai": "^4.47.1",
157+
"pg": "^8.11.5",
144158
"pino": "^9.1.0",
145159
"polished": "^4.3.1",
146160
"posthog-js": "^1.135.2",
@@ -163,6 +177,7 @@
163177
"semver": "^7.6.2",
164178
"sharp": "^0.33.4",
165179
"superjson": "^2.2.1",
180+
"svix": "^1.24.0",
166181
"swr": "^2.2.5",
167182
"systemjs": "^6.15.1",
168183
"ts-md5": "^1.3.1",
@@ -171,6 +186,7 @@
171186
"use-merge-value": "^1.2.0",
172187
"utility-types": "^3.11.0",
173188
"uuid": "^10.0.0",
189+
"ws": "^8.17.0",
174190
"y-protocols": "^1.0.6",
175191
"y-webrtc": "^10.3.0",
176192
"yaml": "^2.4.2",
@@ -200,19 +216,23 @@
200216
"@types/lodash-es": "^4.17.12",
201217
"@types/node": "^20.12.12",
202218
"@types/numeral": "^2.0.5",
219+
"@types/pg": "^8.11.6",
203220
"@types/react": "^18.3.3",
204221
"@types/react-dom": "^18.3.0",
205222
"@types/rtl-detect": "^1.0.3",
206223
"@types/semver": "^7.5.8",
207224
"@types/systemjs": "^6.13.5",
208225
"@types/ua-parser-js": "^0.7.39",
209226
"@types/uuid": "^9.0.8",
227+
"@types/ws": "^8.5.10",
210228
"@umijs/lint": "^4.2.5",
211229
"@vitest/coverage-v8": "~1.2.2",
212230
"ajv-keywords": "^5.1.0",
213231
"commitlint": "^19.3.0",
214232
"consola": "^3.2.3",
233+
"dotenv": "^16.4.5",
215234
"dpdm": "^3.14.0",
235+
"drizzle-kit": "^0.21.1",
216236
"eslint": "^8.57.0",
217237
"eslint-plugin-mdx": "^2.3.4",
218238
"fake-indexeddb": "^6.0.0",

scripts/migrateServerDB/index.ts

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import * as dotenv from 'dotenv';
2+
import * as migrator from 'drizzle-orm/neon-serverless/migrator';
3+
import { join } from 'node:path';
4+
5+
import { serverDB } from '../../src/database/server/core/db';
6+
7+
// Read the `.env` file if it exists, or a file specified by the
8+
// dotenv_config_path parameter that's passed to Node.js
9+
dotenv.config();
10+
11+
const runMigrations = async () => {
12+
await migrator.migrate(serverDB, {
13+
migrationsFolder: join(__dirname, '../../src/database/server/migrations'),
14+
});
15+
console.log('✅ database migration pass.');
16+
// eslint-disable-next-line unicorn/no-process-exit
17+
process.exit(0);
18+
};
19+
20+
let connectionString = process.env.DATABASE_URL;
21+
22+
// only migrate database if the connection string is available
23+
if (connectionString) {
24+
// eslint-disable-next-line unicorn/prefer-top-level-await
25+
runMigrations().catch((err) => {
26+
console.error('❌ Database migrate failed:', err);
27+
// eslint-disable-next-line unicorn/no-process-exit
28+
process.exit(1);
29+
});
30+
}

src/app/(main)/(mobile)/me/(home)/features/useCategory.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next';
66
import { CellProps } from '@/components/Cell';
77
import { enableAuth } from '@/const/auth';
88
import { DISCORD, DOCUMENTS, FEEDBACK } from '@/const/url';
9+
import { isServerMode } from '@/const/version';
910
import { usePWAInstall } from '@/hooks/usePWAInstall';
1011
import { useUserStore } from '@/store/user';
1112
import { authSelectors } from '@/store/user/slices/auth/selectors';
@@ -109,7 +110,7 @@ export const useCategory = () => {
109110

110111
/* ↑ cloud slot ↑ */
111112
...(canInstall ? pwa : []),
112-
...(isLogin ? data : []),
113+
...(isLogin && !isServerMode ? data : []),
113114
...helps,
114115
].filter(Boolean) as CellProps[];
115116

0 commit comments

Comments
 (0)