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 0000000..1490dad Binary files /dev/null and b/packages/drizzle-pkg/db.sqlite differ 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); +}