From 5943a994a2bd3f5ef9ec9b5cb176d358dcad5e05 Mon Sep 17 00:00:00 2001 From: npmrun <1549469775@qq.com> Date: Fri, 17 Apr 2026 09:24:50 +0800 Subject: [PATCH] refactor: migrate database from PostgreSQL to SQLite and update related configurations Transition the database from PostgreSQL to SQLite, updating migration files, database connection settings, and schema definitions accordingly. Adjust package dependencies to reflect the new database type and ensure compatibility. Remove obsolete migration files and snapshots related to PostgreSQL. Enhance error handling in authentication and configuration services to accommodate the new database structure. --- build-files/run.sh | 2 +- bun.lock | 9 +- package.json | 8 +- packages/drizzle-pkg/database/sqlite/db.ts | 12 + .../drizzle-pkg/database/sqlite/schema/auth.ts | 28 +++ .../drizzle-pkg/database/sqlite/schema/config.ts | 35 +++ packages/drizzle-pkg/db.sqlite | Bin 0 -> 53248 bytes packages/drizzle-pkg/drizzle.config.ts | 6 +- packages/drizzle-pkg/lib/db.ts | 2 +- packages/drizzle-pkg/lib/schema/auth.ts | 2 +- packages/drizzle-pkg/lib/schema/config.ts | 2 +- packages/drizzle-pkg/migrations/0000_init.sql | 48 +++- .../drizzle-pkg/migrations/0001_auth-sessions.sql | 8 - .../0002_auth-sessions-quality-fixes.sql | 2 - .../drizzle-pkg/migrations/0003_config-module.sql | 18 -- .../drizzle-pkg/migrations/meta/0000_snapshot.json | 252 ++++++++++++++++--- .../drizzle-pkg/migrations/meta/0001_snapshot.json | 142 ----------- .../drizzle-pkg/migrations/meta/0002_snapshot.json | 158 ------------ .../drizzle-pkg/migrations/meta/0003_snapshot.json | 278 --------------------- packages/drizzle-pkg/migrations/meta/_journal.json | 27 +- scripts/migrate-test.sh | 2 +- server/service/auth/errors.ts | 2 +- server/service/auth/index.ts | 46 +--- server/service/config/errors.ts | 2 +- server/utils/db-unique-constraint.ts | 82 ++++++ 25 files changed, 436 insertions(+), 737 deletions(-) create mode 100644 packages/drizzle-pkg/database/sqlite/schema/auth.ts create mode 100644 packages/drizzle-pkg/database/sqlite/schema/config.ts create mode 100644 packages/drizzle-pkg/db.sqlite delete mode 100644 packages/drizzle-pkg/migrations/0001_auth-sessions.sql delete mode 100644 packages/drizzle-pkg/migrations/0002_auth-sessions-quality-fixes.sql delete mode 100644 packages/drizzle-pkg/migrations/0003_config-module.sql delete mode 100644 packages/drizzle-pkg/migrations/meta/0001_snapshot.json delete mode 100644 packages/drizzle-pkg/migrations/meta/0002_snapshot.json delete mode 100644 packages/drizzle-pkg/migrations/meta/0003_snapshot.json create mode 100644 server/utils/db-unique-constraint.ts diff --git a/build-files/run.sh b/build-files/run.sh index 6dda8a2..ba1bc71 100644 --- a/build-files/run.sh +++ b/build-files/run.sh @@ -4,5 +4,5 @@ if [ -f .env ]; then export $(grep -v '^#' .env | xargs) fi -node server/migrate-pg.js migrations +node server/migrate-sqlite.js migrations node server/index.mjs \ No newline at end of file diff --git a/bun.lock b/bun.lock index f59001e..fedc0e1 100644 --- a/bun.lock +++ b/bun.lock @@ -5,8 +5,8 @@ "": { "name": "person-panel", "dependencies": { - "@nuxt/ui": "^4.6.1", - "bcryptjs": "^3.0.3", + "@nuxt/ui": "4.6.1", + "bcryptjs": "3.0.3", "better-sqlite3": "^12.9.0", "dotenv": "17.4.1", "drizzle-orm": "0.45.2", @@ -26,9 +26,10 @@ "zod": "4.3.6", }, "devDependencies": { - "@types/better-sqlite3": "^7.6.13", + "@types/better-sqlite3": "7.6.13", "@types/multer": "2.1.0", "@types/pg": "8.20.0", + "bun-types": "1.3.12", "drizzle-kit": "0.31.10", "tsx": "4.21.0", "typescript": "6.0.2", @@ -824,6 +825,8 @@ "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + "bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="], + "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], "busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="], diff --git a/package.json b/package.json index 0fe5a49..d9af83f 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,8 @@ "postinstall": "nuxt prepare" }, "dependencies": { - "@nuxt/ui": "^4.6.1", - "bcryptjs": "^3.0.3", + "@nuxt/ui": "4.6.1", + "bcryptjs": "3.0.3", "better-sqlite3": "^12.9.0", "dotenv": "17.4.1", "drizzle-orm": "0.45.2", @@ -40,10 +40,10 @@ "zod": "4.3.6" }, "devDependencies": { - "@types/better-sqlite3": "^7.6.13", + "@types/better-sqlite3": "7.6.13", "@types/multer": "2.1.0", "@types/pg": "8.20.0", - "bun-types": "^1.3.12", + "bun-types": "1.3.12", "drizzle-kit": "0.31.10", "tsx": "4.21.0", "typescript": "6.0.2" diff --git a/packages/drizzle-pkg/database/sqlite/db.ts b/packages/drizzle-pkg/database/sqlite/db.ts index 375f1f2..80c0b91 100644 --- a/packages/drizzle-pkg/database/sqlite/db.ts +++ b/packages/drizzle-pkg/database/sqlite/db.ts @@ -1,10 +1,22 @@ import { drizzle } from 'drizzle-orm/libsql'; +import path from 'path'; if (process.env.NODE_ENV === 'production') { // 打包时需要保证migrator被引入 import('drizzle-orm/better-sqlite3/migrator') } +const tempCwd = path.resolve(process.cwd(), 'packages/drizzle-pkg'); + +let dbUrl = process.env.DATABASE_URL; +if (dbUrl && dbUrl.startsWith('file:')) { + let filePath = dbUrl.slice(5); + if (!path.isAbsolute(filePath)) { + filePath = path.resolve(tempCwd, filePath); + process.env.DATABASE_URL = 'file:' + filePath; + } +} + const _db = drizzle(process.env.DATABASE_URL!); export { _db as dbGlobal } \ No newline at end of file diff --git a/packages/drizzle-pkg/database/sqlite/schema/auth.ts b/packages/drizzle-pkg/database/sqlite/schema/auth.ts new file mode 100644 index 0000000..1292cc5 --- /dev/null +++ b/packages/drizzle-pkg/database/sqlite/schema/auth.ts @@ -0,0 +1,28 @@ +import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; + +export const users = sqliteTable("users", { + id: integer().primaryKey(), + username: text().notNull().unique(), + email: text(), + nickname: text(), + password: text().notNull(), + avatar: text(), + createdAt: integer("created_at", { mode: "timestamp_ms" }).defaultNow().notNull(), + updatedAt: integer("updated_at", { mode: "timestamp_ms" }) + .defaultNow() + .$onUpdate(() => new Date()) + .notNull(), +}); + +export const sessions = sqliteTable( + "sessions", + { + id: text().primaryKey(), + userId: integer("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(), + createdAt: integer("created_at", { mode: "timestamp_ms" }).defaultNow().notNull(), + }, + (table) => [index("sessions_user_id_idx").on(table.userId)], +); diff --git a/packages/drizzle-pkg/database/sqlite/schema/config.ts b/packages/drizzle-pkg/database/sqlite/schema/config.ts new file mode 100644 index 0000000..aeed642 --- /dev/null +++ b/packages/drizzle-pkg/database/sqlite/schema/config.ts @@ -0,0 +1,35 @@ +import { index, integer, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core"; +import { users } from "./auth"; + +export const appConfigs = sqliteTable("app_configs", { + key: text().primaryKey(), + value: text().notNull(), + valueType: text("value_type").notNull(), + updatedAt: integer("updated_at", { mode: "timestamp_ms" }) + .defaultNow() + .$onUpdate(() => new Date()) + .notNull(), +}); + +export const userConfigs = sqliteTable( + "user_configs", + { + userId: integer("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + key: text().notNull(), + value: text().notNull(), + valueType: text("value_type").notNull(), + updatedAt: integer("updated_at", { mode: "timestamp_ms" }) + .defaultNow() + .$onUpdate(() => new Date()) + .notNull(), + }, + (table) => [ + primaryKey({ + name: "user_configs_user_id_key_pk", + columns: [table.userId, table.key], + }), + index("user_configs_user_id_idx").on(table.userId), + ], +); diff --git a/packages/drizzle-pkg/db.sqlite b/packages/drizzle-pkg/db.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..1490dad19eb63460e506bc76394746ac57f507ad GIT binary patch literal 53248 zcmeI*!Ef4D90zc_gg7S2sI-HnRgtW=QNz@s3`kd5bXDi?f3b;-xCHy zHaCivOXYg2(OycA-BG&{?` z+Hgs&LBm@K&rcrhY-{oBqIPO?G4XhVvSqt;mv-dR+Lm0Z6pOc6KluDj3LXH|CFCUL2$6SsQ2t7UCvu~OWU=M3Vwb8~y$hDB_XJe#{^x1Qcg$n$bK zlSwK$^^TJGJeSQRy-6lcuz^G(d|d10!h^-~j(lI+IdQrz`;ADj)~u|RwZdx2s}DoU zWo<<(Yo#S^v+v_n=c=Bt=U-W0_CjU1WMa5h%l68awW7w#*fAT#q_KoLIWFCwftq{5+@>OG$jq#HN*mtXiHB<*(4bwab^U;slktUC;ZDAfKi zOLY{(6-y}MxKy|?OfA$*Jr!Dr?FoXUN__9r<8hzQcm4F5ydk94q3@HjRY@1Xc3 z{!pAH>lpUc00ryeVwz)t_%d{`dO=7_eM%lv`l$ZD;E}v;nZYVC5n%T^XODY ziQ);qY04!3!_L~jo;s^E!d_%}=kxvvJ|F*$V>e6?fB*y_009U< z00Izz00bZa0SJtazhZ!D-uYM3dtMwN`3%Vg^LtdcS6N{(iz znMp3ttV-&&yqZp#StFCL8EInjhp+#Ba3{vv#mE2Q*bNf|AOHafKmY;|fB*y_009U< z00Ng#AQIt3{{sK392fuN5;hP`f&c^{009U<00Izz00bZa0SH_+0fmoN#Aq~|N@sJl zR-3OARhiGI#=?AFRr2$SO3ZXFnN8@7l$k&4*2Om8J_z+sTrW-g(m4-O<*!57zcp*Y=vU z+{kTOdBU#zkG%J;|7U;uzyJ4+i@*Dr4aB}7009U<00Izz00bZa0SG_<0uVTNfmr04 zc=SF(@cVz~z7K2=0uX=z1Rwwb2tWV=5P$##AOL}p5eUBjKQae|1AzbpAOHafKmY;| zfB*y_009U<;M@i9`Tx0-VuKKX00bZa0SG_<0uX=z1Rwx`krTk@|05R?91sK`009U< u00Izz00bZa0SG|g+y!v{f9|B%AOs))0SG_<0uX=z1Rwwb2tZ)u1pWm&5)z>R literal 0 HcmV?d00001 diff --git a/packages/drizzle-pkg/drizzle.config.ts b/packages/drizzle-pkg/drizzle.config.ts index b7b9a4f..fd3f32c 100644 --- a/packages/drizzle-pkg/drizzle.config.ts +++ b/packages/drizzle-pkg/drizzle.config.ts @@ -3,9 +3,9 @@ import { defineConfig } from 'drizzle-kit'; export default defineConfig({ out: './migrations', - schema: './database/pg/schema/*', - dialect: 'postgresql', + schema: './database/sqlite/schema/*', + dialect: 'sqlite', dbCredentials: { url: process.env.DATABASE_URL! - }, + } }); diff --git a/packages/drizzle-pkg/lib/db.ts b/packages/drizzle-pkg/lib/db.ts index dda9669..eb98908 100644 --- a/packages/drizzle-pkg/lib/db.ts +++ b/packages/drizzle-pkg/lib/db.ts @@ -1 +1 @@ -export { dbGlobal } from '../database/pg/db' \ No newline at end of file +export { dbGlobal } from '../database/sqlite/db' \ No newline at end of file diff --git a/packages/drizzle-pkg/lib/schema/auth.ts b/packages/drizzle-pkg/lib/schema/auth.ts index 6b43ce3..c88c458 100644 --- a/packages/drizzle-pkg/lib/schema/auth.ts +++ b/packages/drizzle-pkg/lib/schema/auth.ts @@ -1 +1 @@ -export { users, sessions } from '../../database/pg/schema/auth' \ No newline at end of file +export { users, sessions } from '../../database/sqlite/schema/auth' \ No newline at end of file diff --git a/packages/drizzle-pkg/lib/schema/config.ts b/packages/drizzle-pkg/lib/schema/config.ts index d7a6a75..ba7a3e8 100644 --- a/packages/drizzle-pkg/lib/schema/config.ts +++ b/packages/drizzle-pkg/lib/schema/config.ts @@ -1 +1 @@ -export { appConfigs, userConfigs } from "../../database/pg/schema/config"; +export { appConfigs, userConfigs } from "../../database/sqlite/schema/config"; diff --git a/packages/drizzle-pkg/migrations/0000_init.sql b/packages/drizzle-pkg/migrations/0000_init.sql index 74bd058..1656659 100644 --- a/packages/drizzle-pkg/migrations/0000_init.sql +++ b/packages/drizzle-pkg/migrations/0000_init.sql @@ -1,11 +1,39 @@ -CREATE TABLE "users" ( - "id" integer PRIMARY KEY NOT NULL, - "username" varchar NOT NULL, - "email" varchar, - "nickname" varchar, - "password" varchar NOT NULL, - "avatar" varchar, - "created_at" timestamp DEFAULT now() NOT NULL, - "updated_at" timestamp DEFAULT now() NOT NULL, - CONSTRAINT "users_username_unique" UNIQUE("username") +CREATE TABLE `sessions` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` integer NOT NULL, + `expires_at` integer NOT NULL, + `created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade ); +--> statement-breakpoint +CREATE INDEX `sessions_user_id_idx` ON `sessions` (`user_id`);--> statement-breakpoint +CREATE TABLE `users` ( + `id` integer PRIMARY KEY NOT NULL, + `username` text NOT NULL, + `email` text, + `nickname` text, + `password` text NOT NULL, + `avatar` text, + `created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL, + `updated_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX `users_username_unique` ON `users` (`username`);--> statement-breakpoint +CREATE TABLE `app_configs` ( + `key` text PRIMARY KEY NOT NULL, + `value` text NOT NULL, + `value_type` text NOT NULL, + `updated_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL +); +--> statement-breakpoint +CREATE TABLE `user_configs` ( + `user_id` integer NOT NULL, + `key` text NOT NULL, + `value` text NOT NULL, + `value_type` text NOT NULL, + `updated_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL, + PRIMARY KEY(`user_id`, `key`), + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `user_configs_user_id_idx` ON `user_configs` (`user_id`); \ No newline at end of file diff --git a/packages/drizzle-pkg/migrations/0001_auth-sessions.sql b/packages/drizzle-pkg/migrations/0001_auth-sessions.sql deleted file mode 100644 index a7a05b4..0000000 --- a/packages/drizzle-pkg/migrations/0001_auth-sessions.sql +++ /dev/null @@ -1,8 +0,0 @@ -CREATE TABLE "sessions" ( - "id" varchar PRIMARY KEY NOT NULL, - "user_id" integer NOT NULL, - "expires_at" timestamp NOT NULL, - "created_at" timestamp DEFAULT now() NOT NULL -); ---> statement-breakpoint -ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/packages/drizzle-pkg/migrations/0002_auth-sessions-quality-fixes.sql b/packages/drizzle-pkg/migrations/0002_auth-sessions-quality-fixes.sql deleted file mode 100644 index d449591..0000000 --- a/packages/drizzle-pkg/migrations/0002_auth-sessions-quality-fixes.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE "sessions" ALTER COLUMN "expires_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint -CREATE INDEX "sessions_user_id_idx" ON "sessions" USING btree ("user_id"); \ No newline at end of file diff --git a/packages/drizzle-pkg/migrations/0003_config-module.sql b/packages/drizzle-pkg/migrations/0003_config-module.sql deleted file mode 100644 index 22fdaf7..0000000 --- a/packages/drizzle-pkg/migrations/0003_config-module.sql +++ /dev/null @@ -1,18 +0,0 @@ -CREATE TABLE "app_configs" ( - "key" varchar PRIMARY KEY NOT NULL, - "value" varchar NOT NULL, - "value_type" varchar NOT NULL, - "updated_at" timestamp DEFAULT now() NOT NULL -); ---> statement-breakpoint -CREATE TABLE "user_configs" ( - "user_id" integer NOT NULL, - "key" varchar NOT NULL, - "value" varchar NOT NULL, - "value_type" varchar NOT NULL, - "updated_at" timestamp DEFAULT now() NOT NULL, - CONSTRAINT "user_configs_user_id_key_pk" PRIMARY KEY("user_id","key") -); ---> statement-breakpoint -ALTER TABLE "user_configs" ADD CONSTRAINT "user_configs_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -CREATE INDEX "user_configs_user_id_idx" ON "user_configs" USING btree ("user_id"); \ No newline at end of file diff --git a/packages/drizzle-pkg/migrations/meta/0000_snapshot.json b/packages/drizzle-pkg/migrations/meta/0000_snapshot.json index ac0b164..3111408 100644 --- a/packages/drizzle-pkg/migrations/meta/0000_snapshot.json +++ b/packages/drizzle-pkg/migrations/meta/0000_snapshot.json @@ -1,90 +1,270 @@ { - "id": "25840823-aa2a-4e32-a6b6-70bb1e27348e", + "version": "6", + "dialect": "sqlite", + "id": "0c82e00e-7d76-4479-a196-ceb6eb02ad3f", "prevId": "00000000-0000-0000-0000-000000000000", - "version": "7", - "dialect": "postgresql", "tables": { - "public.users": { + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": { + "sessions_user_id_idx": { + "name": "sessions_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { "name": "users", - "schema": "", "columns": { "id": { "name": "id", "type": "integer", "primaryKey": true, - "notNull": true + "notNull": true, + "autoincrement": false }, "username": { "name": "username", - "type": "varchar", + "type": "text", "primaryKey": false, - "notNull": true + "notNull": true, + "autoincrement": false }, "email": { "name": "email", - "type": "varchar", + "type": "text", "primaryKey": false, - "notNull": false + "notNull": false, + "autoincrement": false }, "nickname": { "name": "nickname", - "type": "varchar", + "type": "text", "primaryKey": false, - "notNull": false + "notNull": false, + "autoincrement": false }, "password": { "name": "password", - "type": "varchar", + "type": "text", "primaryKey": false, - "notNull": true + "notNull": true, + "autoincrement": false }, "avatar": { "name": "avatar", - "type": "varchar", + "type": "text", "primaryKey": false, - "notNull": false + "notNull": false, + "autoincrement": false }, "created_at": { "name": "created_at", - "type": "timestamp", + "type": "integer", "primaryKey": false, "notNull": true, - "default": "now()" + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" }, "updated_at": { "name": "updated_at", - "type": "timestamp", + "type": "integer", "primaryKey": false, "notNull": true, - "default": "now()" + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" } }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { + "indexes": { "users_username_unique": { "name": "users_username_unique", - "nullsNotDistinct": false, "columns": [ "username" - ] + ], + "isUnique": true } }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "app_configs": { + "name": "app_configs", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value_type": { + "name": "value_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_configs": { + "name": "user_configs", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value_type": { + "name": "value_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": { + "user_configs_user_id_idx": { + "name": "user_configs_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_configs_user_id_users_id_fk": { + "name": "user_configs_user_id_users_id_fk", + "tableFrom": "user_configs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_configs_user_id_key_pk": { + "columns": [ + "user_id", + "key" + ], + "name": "user_configs_user_id_key_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} } }, - "enums": {}, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, "views": {}, + "enums": {}, "_meta": { - "columns": {}, "schemas": {}, - "tables": {} + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} } } \ No newline at end of file diff --git a/packages/drizzle-pkg/migrations/meta/0001_snapshot.json b/packages/drizzle-pkg/migrations/meta/0001_snapshot.json deleted file mode 100644 index de6938d..0000000 --- a/packages/drizzle-pkg/migrations/meta/0001_snapshot.json +++ /dev/null @@ -1,142 +0,0 @@ -{ - "id": "b2828e1e-04fb-495c-9a25-233d062ee460", - "prevId": "25840823-aa2a-4e32-a6b6-70bb1e27348e", - "version": "7", - "dialect": "postgresql", - "tables": { - "public.sessions": { - "name": "sessions", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "varchar", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "sessions_user_id_users_id_fk": { - "name": "sessions_user_id_users_id_fk", - "tableFrom": "sessions", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.users": { - "name": "users", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true - }, - "username": { - "name": "username", - "type": "varchar", - "primaryKey": false, - "notNull": true - }, - "email": { - "name": "email", - "type": "varchar", - "primaryKey": false, - "notNull": false - }, - "nickname": { - "name": "nickname", - "type": "varchar", - "primaryKey": false, - "notNull": false - }, - "password": { - "name": "password", - "type": "varchar", - "primaryKey": false, - "notNull": true - }, - "avatar": { - "name": "avatar", - "type": "varchar", - "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": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "users_username_unique": { - "name": "users_username_unique", - "nullsNotDistinct": false, - "columns": [ - "username" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - } - }, - "enums": {}, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} \ No newline at end of file diff --git a/packages/drizzle-pkg/migrations/meta/0002_snapshot.json b/packages/drizzle-pkg/migrations/meta/0002_snapshot.json deleted file mode 100644 index 3acec7a..0000000 --- a/packages/drizzle-pkg/migrations/meta/0002_snapshot.json +++ /dev/null @@ -1,158 +0,0 @@ -{ - "id": "3caf12bf-ffef-4a49-b1ed-9af7f01551f4", - "prevId": "b2828e1e-04fb-495c-9a25-233d062ee460", - "version": "7", - "dialect": "postgresql", - "tables": { - "public.sessions": { - "name": "sessions", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "varchar", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "sessions_user_id_idx": { - "name": "sessions_user_id_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "sessions_user_id_users_id_fk": { - "name": "sessions_user_id_users_id_fk", - "tableFrom": "sessions", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.users": { - "name": "users", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true - }, - "username": { - "name": "username", - "type": "varchar", - "primaryKey": false, - "notNull": true - }, - "email": { - "name": "email", - "type": "varchar", - "primaryKey": false, - "notNull": false - }, - "nickname": { - "name": "nickname", - "type": "varchar", - "primaryKey": false, - "notNull": false - }, - "password": { - "name": "password", - "type": "varchar", - "primaryKey": false, - "notNull": true - }, - "avatar": { - "name": "avatar", - "type": "varchar", - "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": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "users_username_unique": { - "name": "users_username_unique", - "nullsNotDistinct": false, - "columns": [ - "username" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - } - }, - "enums": {}, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} \ No newline at end of file diff --git a/packages/drizzle-pkg/migrations/meta/0003_snapshot.json b/packages/drizzle-pkg/migrations/meta/0003_snapshot.json deleted file mode 100644 index abc32ac..0000000 --- a/packages/drizzle-pkg/migrations/meta/0003_snapshot.json +++ /dev/null @@ -1,278 +0,0 @@ -{ - "id": "5ad5cea4-52fb-4e68-813a-f1b125bcba51", - "prevId": "3caf12bf-ffef-4a49-b1ed-9af7f01551f4", - "version": "7", - "dialect": "postgresql", - "tables": { - "public.sessions": { - "name": "sessions", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "varchar", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "sessions_user_id_idx": { - "name": "sessions_user_id_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "sessions_user_id_users_id_fk": { - "name": "sessions_user_id_users_id_fk", - "tableFrom": "sessions", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.users": { - "name": "users", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true - }, - "username": { - "name": "username", - "type": "varchar", - "primaryKey": false, - "notNull": true - }, - "email": { - "name": "email", - "type": "varchar", - "primaryKey": false, - "notNull": false - }, - "nickname": { - "name": "nickname", - "type": "varchar", - "primaryKey": false, - "notNull": false - }, - "password": { - "name": "password", - "type": "varchar", - "primaryKey": false, - "notNull": true - }, - "avatar": { - "name": "avatar", - "type": "varchar", - "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": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "users_username_unique": { - "name": "users_username_unique", - "nullsNotDistinct": false, - "columns": [ - "username" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.app_configs": { - "name": "app_configs", - "schema": "", - "columns": { - "key": { - "name": "key", - "type": "varchar", - "primaryKey": true, - "notNull": true - }, - "value": { - "name": "value", - "type": "varchar", - "primaryKey": false, - "notNull": true - }, - "value_type": { - "name": "value_type", - "type": "varchar", - "primaryKey": false, - "notNull": true - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.user_configs": { - "name": "user_configs", - "schema": "", - "columns": { - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "key": { - "name": "key", - "type": "varchar", - "primaryKey": false, - "notNull": true - }, - "value": { - "name": "value", - "type": "varchar", - "primaryKey": false, - "notNull": true - }, - "value_type": { - "name": "value_type", - "type": "varchar", - "primaryKey": false, - "notNull": true - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "user_configs_user_id_idx": { - "name": "user_configs_user_id_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "user_configs_user_id_users_id_fk": { - "name": "user_configs_user_id_users_id_fk", - "tableFrom": "user_configs", - "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": { - "user_configs_user_id_key_pk": { - "name": "user_configs_user_id_key_pk", - "columns": [ - "user_id", - "key" - ] - } - }, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - } - }, - "enums": {}, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} \ No newline at end of file diff --git a/packages/drizzle-pkg/migrations/meta/_journal.json b/packages/drizzle-pkg/migrations/meta/_journal.json index 60f9a55..0637341 100644 --- a/packages/drizzle-pkg/migrations/meta/_journal.json +++ b/packages/drizzle-pkg/migrations/meta/_journal.json @@ -1,34 +1,13 @@ { "version": "7", - "dialect": "postgresql", + "dialect": "sqlite", "entries": [ { "idx": 0, - "version": "7", - "when": 1776329125490, + "version": "6", + "when": 1776386788654, "tag": "0000_init", "breakpoints": true - }, - { - "idx": 1, - "version": "7", - "when": 1776336996454, - "tag": "0001_auth-sessions", - "breakpoints": true - }, - { - "idx": 2, - "version": "7", - "when": 1776337212081, - "tag": "0002_auth-sessions-quality-fixes", - "breakpoints": true - }, - { - "idx": 3, - "version": "7", - "when": 1776348953237, - "tag": "0003_config-module", - "breakpoints": true } ] } \ No newline at end of file diff --git a/scripts/migrate-test.sh b/scripts/migrate-test.sh index f2a4a7a..7cb9099 100644 --- a/scripts/migrate-test.sh +++ b/scripts/migrate-test.sh @@ -4,4 +4,4 @@ fi echo "DATABASE_URL: $DATABASE_URL" -node build-files/migrate/migrate-pg.js packages/drizzle-pkg/migrations +node build-files/migrate/migrate-sqlite.js packages/drizzle-pkg/migrations diff --git a/server/service/auth/errors.ts b/server/service/auth/errors.ts index 638690d..e009136 100644 --- a/server/service/auth/errors.ts +++ b/server/service/auth/errors.ts @@ -27,7 +27,7 @@ export function toPublicAuthError(err: unknown) { statusMessage: err.message, }); } - return createError({ + return createError(err instanceof Error ? err : { statusCode: 500, statusMessage: "服务器繁忙,请稍后重试", }); diff --git a/server/service/auth/index.ts b/server/service/auth/index.ts index a3d7327..8844dec 100644 --- a/server/service/auth/index.ts +++ b/server/service/auth/index.ts @@ -65,17 +65,6 @@ function authFailedError() { return new AuthFailedError("用户名或密码错误"); } -function unwrapDbError(err: unknown): unknown { - if (!(err instanceof Error)) { - return err; - } - if (!("cause" in err)) { - return err; - } - const cause = (err as { cause?: unknown }).cause; - return cause ?? err; -} - async function createSession(userId: number) { const sessionId = randomUUID(); const expiresAt = new Date(Date.now() + SESSION_EXPIRE_MS); @@ -96,37 +85,6 @@ async function getNextUserId() { return (row?.maxId ?? 0) + 1; } -function isPgUniqueViolation(err: unknown) { - const dbError = unwrapDbError(err); - if (!(dbError instanceof Error)) { - return false; - } - return "code" in dbError && (dbError as { code?: string }).code === "23505"; -} - -function getPgConstraint(err: unknown) { - const dbError = unwrapDbError(err); - if (!(dbError instanceof Error)) { - return ""; - } - if (!("constraint" in dbError)) { - return ""; - } - return ((dbError as { constraint?: string }).constraint ?? "").toLowerCase(); -} - -function isUsernameConflict(err: unknown) { - return isPgUniqueViolation(err) && getPgConstraint(err).includes("username"); -} - -function isUserIdConflict(err: unknown) { - if (!isPgUniqueViolation(err)) { - return false; - } - const constraint = getPgConstraint(err); - return constraint.includes("pkey") || constraint.includes("id"); -} - async function insertUserWithRetry(username: string, passwordHash: string): Promise { const maxRetry = 5; for (let attempt = 0; attempt < maxRetry; attempt++) { @@ -145,10 +103,10 @@ async function insertUserWithRetry(username: string, passwordHash: string): Prom }); return newUser as MinimalUser; } catch (err) { - if (isUsernameConflict(err)) { + if (isUniqueConflictOnField(err, "username")) { throw new AuthConflictError("用户名已存在"); } - if (isUserIdConflict(err) && attempt < maxRetry - 1) { + if (isUniqueConflictExceptField(err, "username") && attempt < maxRetry - 1) { continue; } throw err; diff --git a/server/service/config/errors.ts b/server/service/config/errors.ts index 20ebf58..494352a 100644 --- a/server/service/config/errors.ts +++ b/server/service/config/errors.ts @@ -23,7 +23,7 @@ export function toPublicConfigError(err: unknown) { data: { code: err.code }, }); } - return createError({ + return createError(err instanceof Error ? err : { statusCode: 500, statusMessage: "服务器繁忙,请稍后重试", }); diff --git a/server/utils/db-unique-constraint.ts b/server/utils/db-unique-constraint.ts new file mode 100644 index 0000000..8f51266 --- /dev/null +++ b/server/utils/db-unique-constraint.ts @@ -0,0 +1,82 @@ +/** + * 识别各驱动常见的「唯一 / 重复键」错误,供任意 service 复用。 + * 具体业务(如用户名冲突 vs 主键重试)用 {@link uniqueConstraintTouchesField} 再区分即可。 + */ + +/** 遍历包装错误(如 Drizzle)与底层驱动的整条 cause 链 */ +export function* eachErrorInChain(err: unknown): Generator { + let current: unknown = err; + const seen = new Set(); + while (current instanceof Error && !seen.has(current)) { + seen.add(current); + yield current; + current = "cause" in current ? (current as { cause?: unknown }).cause : undefined; + } +} + +function escapeRegExp(s: string) { + return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +/** + * 报错文案或 constraint 名是否涉及该字段。 + * message 用词边界,避免 `guid` 误匹配 `id`;constraint 多为 `users_username_key` 式拼接,用子串匹配。 + */ +export function uniqueConstraintTouchesField(err: unknown, field: string): boolean { + const fl = field.toLowerCase(); + const re = new RegExp(`\\b${escapeRegExp(field)}\\b`, "i"); + for (const e of eachErrorInChain(err)) { + if (re.test(e.message)) { + return true; + } + if ("constraint" in e) { + const c = String((e as { constraint?: unknown }).constraint ?? "").toLowerCase(); + if (c.includes(fl)) { + return true; + } + } + } + return false; +} + +export function isUniqueConstraintViolation(err: unknown): boolean { + for (const e of eachErrorInChain(err)) { + const code = "code" in e ? String((e as { code?: unknown }).code ?? "") : ""; + const errno = "errno" in e ? (e as { errno?: unknown }).errno : undefined; + + if (code === "23505") { + return true; + } + if (code === "ER_DUP_ENTRY") { + return true; + } + if (typeof errno === "number" && errno === 1062) { + return true; + } + if (code.startsWith("SQLITE_CONSTRAINT")) { + return true; + } + + const m = e.message.toLowerCase(); + if (m.includes("unique constraint failed")) { + return true; + } + if (m.includes("duplicate entry")) { + return true; + } + if (m.includes("duplicate key") && m.includes("unique")) { + return true; + } + } + return false; +} + +/** 唯一冲突且与指定字段相关(例如用户名占用) */ +export function isUniqueConflictOnField(err: unknown, field: string): boolean { + return isUniqueConstraintViolation(err) && uniqueConstraintTouchesField(err, field); +} + +/** 唯一冲突且与指定字段无关(例如除 username 外仅有主键时的 id 撞车重试) */ +export function isUniqueConflictExceptField(err: unknown, field: string): boolean { + return isUniqueConstraintViolation(err) && !uniqueConstraintTouchesField(err, field); +}