Browse Source

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.
feat/auth-access-control
npmrun 1 day ago
parent
commit
5943a994a2
  1. 2
      build-files/run.sh
  2. 9
      bun.lock
  3. 8
      package.json
  4. 12
      packages/drizzle-pkg/database/sqlite/db.ts
  5. 28
      packages/drizzle-pkg/database/sqlite/schema/auth.ts
  6. 35
      packages/drizzle-pkg/database/sqlite/schema/config.ts
  7. BIN
      packages/drizzle-pkg/db.sqlite
  8. 6
      packages/drizzle-pkg/drizzle.config.ts
  9. 2
      packages/drizzle-pkg/lib/db.ts
  10. 2
      packages/drizzle-pkg/lib/schema/auth.ts
  11. 2
      packages/drizzle-pkg/lib/schema/config.ts
  12. 48
      packages/drizzle-pkg/migrations/0000_init.sql
  13. 8
      packages/drizzle-pkg/migrations/0001_auth-sessions.sql
  14. 2
      packages/drizzle-pkg/migrations/0002_auth-sessions-quality-fixes.sql
  15. 18
      packages/drizzle-pkg/migrations/0003_config-module.sql
  16. 252
      packages/drizzle-pkg/migrations/meta/0000_snapshot.json
  17. 142
      packages/drizzle-pkg/migrations/meta/0001_snapshot.json
  18. 158
      packages/drizzle-pkg/migrations/meta/0002_snapshot.json
  19. 278
      packages/drizzle-pkg/migrations/meta/0003_snapshot.json
  20. 27
      packages/drizzle-pkg/migrations/meta/_journal.json
  21. 2
      scripts/migrate-test.sh
  22. 2
      server/service/auth/errors.ts
  23. 46
      server/service/auth/index.ts
  24. 2
      server/service/config/errors.ts
  25. 82
      server/utils/db-unique-constraint.ts

2
build-files/run.sh

@ -4,5 +4,5 @@ if [ -f .env ]; then
export $(grep -v '^#' .env | xargs) export $(grep -v '^#' .env | xargs)
fi fi
node server/migrate-pg.js migrations node server/migrate-sqlite.js migrations
node server/index.mjs node server/index.mjs

9
bun.lock

@ -5,8 +5,8 @@
"": { "": {
"name": "person-panel", "name": "person-panel",
"dependencies": { "dependencies": {
"@nuxt/ui": "^4.6.1", "@nuxt/ui": "4.6.1",
"bcryptjs": "^3.0.3", "bcryptjs": "3.0.3",
"better-sqlite3": "^12.9.0", "better-sqlite3": "^12.9.0",
"dotenv": "17.4.1", "dotenv": "17.4.1",
"drizzle-orm": "0.45.2", "drizzle-orm": "0.45.2",
@ -26,9 +26,10 @@
"zod": "4.3.6", "zod": "4.3.6",
}, },
"devDependencies": { "devDependencies": {
"@types/better-sqlite3": "^7.6.13", "@types/better-sqlite3": "7.6.13",
"@types/multer": "2.1.0", "@types/multer": "2.1.0",
"@types/pg": "8.20.0", "@types/pg": "8.20.0",
"bun-types": "1.3.12",
"drizzle-kit": "0.31.10", "drizzle-kit": "0.31.10",
"tsx": "4.21.0", "tsx": "4.21.0",
"typescript": "6.0.2", "typescript": "6.0.2",
@ -824,6 +825,8 @@
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], "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=="], "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=="], "busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="],

8
package.json

@ -19,8 +19,8 @@
"postinstall": "nuxt prepare" "postinstall": "nuxt prepare"
}, },
"dependencies": { "dependencies": {
"@nuxt/ui": "^4.6.1", "@nuxt/ui": "4.6.1",
"bcryptjs": "^3.0.3", "bcryptjs": "3.0.3",
"better-sqlite3": "^12.9.0", "better-sqlite3": "^12.9.0",
"dotenv": "17.4.1", "dotenv": "17.4.1",
"drizzle-orm": "0.45.2", "drizzle-orm": "0.45.2",
@ -40,10 +40,10 @@
"zod": "4.3.6" "zod": "4.3.6"
}, },
"devDependencies": { "devDependencies": {
"@types/better-sqlite3": "^7.6.13", "@types/better-sqlite3": "7.6.13",
"@types/multer": "2.1.0", "@types/multer": "2.1.0",
"@types/pg": "8.20.0", "@types/pg": "8.20.0",
"bun-types": "^1.3.12", "bun-types": "1.3.12",
"drizzle-kit": "0.31.10", "drizzle-kit": "0.31.10",
"tsx": "4.21.0", "tsx": "4.21.0",
"typescript": "6.0.2" "typescript": "6.0.2"

12
packages/drizzle-pkg/database/sqlite/db.ts

@ -1,10 +1,22 @@
import { drizzle } from 'drizzle-orm/libsql'; import { drizzle } from 'drizzle-orm/libsql';
import path from 'path';
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production') {
// 打包时需要保证migrator被引入 // 打包时需要保证migrator被引入
import('drizzle-orm/better-sqlite3/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!); const _db = drizzle(process.env.DATABASE_URL!);
export { _db as dbGlobal } export { _db as dbGlobal }

28
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)],
);

35
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),
],
);

BIN
packages/drizzle-pkg/db.sqlite

Binary file not shown.

6
packages/drizzle-pkg/drizzle.config.ts

@ -3,9 +3,9 @@ import { defineConfig } from 'drizzle-kit';
export default defineConfig({ export default defineConfig({
out: './migrations', out: './migrations',
schema: './database/pg/schema/*', schema: './database/sqlite/schema/*',
dialect: 'postgresql', dialect: 'sqlite',
dbCredentials: { dbCredentials: {
url: process.env.DATABASE_URL! url: process.env.DATABASE_URL!
}, }
}); });

2
packages/drizzle-pkg/lib/db.ts

@ -1 +1 @@
export { dbGlobal } from '../database/pg/db' export { dbGlobal } from '../database/sqlite/db'

2
packages/drizzle-pkg/lib/schema/auth.ts

@ -1 +1 @@
export { users, sessions } from '../../database/pg/schema/auth' export { users, sessions } from '../../database/sqlite/schema/auth'

2
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";

48
packages/drizzle-pkg/migrations/0000_init.sql

@ -1,11 +1,39 @@
CREATE TABLE "users" ( CREATE TABLE `sessions` (
"id" integer PRIMARY KEY NOT NULL, `id` text PRIMARY KEY NOT NULL,
"username" varchar NOT NULL, `user_id` integer NOT NULL,
"email" varchar, `expires_at` integer NOT NULL,
"nickname" varchar, `created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL,
"password" varchar NOT NULL, FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
"avatar" varchar,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "users_username_unique" UNIQUE("username")
); );
--> 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`);

8
packages/drizzle-pkg/migrations/0001_auth-sessions.sql

@ -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;

2
packages/drizzle-pkg/migrations/0002_auth-sessions-quality-fixes.sql

@ -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");

18
packages/drizzle-pkg/migrations/0003_config-module.sql

@ -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");

252
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", "prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": { "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", "name": "users",
"schema": "",
"columns": { "columns": {
"id": { "id": {
"name": "id", "name": "id",
"type": "integer", "type": "integer",
"primaryKey": true, "primaryKey": true,
"notNull": true "notNull": true,
"autoincrement": false
}, },
"username": { "username": {
"name": "username", "name": "username",
"type": "varchar", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": true "notNull": true,
"autoincrement": false
}, },
"email": { "email": {
"name": "email", "name": "email",
"type": "varchar", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false "notNull": false,
"autoincrement": false
}, },
"nickname": { "nickname": {
"name": "nickname", "name": "nickname",
"type": "varchar", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false "notNull": false,
"autoincrement": false
}, },
"password": { "password": {
"name": "password", "name": "password",
"type": "varchar", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": true "notNull": true,
"autoincrement": false
}, },
"avatar": { "avatar": {
"name": "avatar", "name": "avatar",
"type": "varchar", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false "notNull": false,
"autoincrement": false
}, },
"created_at": { "created_at": {
"name": "created_at", "name": "created_at",
"type": "timestamp", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"default": "now()" "autoincrement": false,
"default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))"
}, },
"updated_at": { "updated_at": {
"name": "updated_at", "name": "updated_at",
"type": "timestamp", "type": "integer",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"default": "now()" "autoincrement": false,
"default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))"
} }
}, },
"indexes": {}, "indexes": {
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"users_username_unique": { "users_username_unique": {
"name": "users_username_unique", "name": "users_username_unique",
"nullsNotDistinct": false,
"columns": [ "columns": [
"username" "username"
] ],
"isUnique": true
} }
}, },
"policies": {}, "foreignKeys": {},
"checkConstraints": {}, "compositePrimaryKeys": {},
"isRLSEnabled": false "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": {}, "views": {},
"enums": {},
"_meta": { "_meta": {
"columns": {},
"schemas": {}, "schemas": {},
"tables": {} "tables": {},
"columns": {}
},
"internal": {
"indexes": {}
} }
} }

142
packages/drizzle-pkg/migrations/meta/0001_snapshot.json

@ -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": {}
}
}

158
packages/drizzle-pkg/migrations/meta/0002_snapshot.json

@ -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": {}
}
}

278
packages/drizzle-pkg/migrations/meta/0003_snapshot.json

@ -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": {}
}
}

27
packages/drizzle-pkg/migrations/meta/_journal.json

@ -1,34 +1,13 @@
{ {
"version": "7", "version": "7",
"dialect": "postgresql", "dialect": "sqlite",
"entries": [ "entries": [
{ {
"idx": 0, "idx": 0,
"version": "7", "version": "6",
"when": 1776329125490, "when": 1776386788654,
"tag": "0000_init", "tag": "0000_init",
"breakpoints": true "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
} }
] ]
} }

2
scripts/migrate-test.sh

@ -4,4 +4,4 @@ fi
echo "DATABASE_URL: $DATABASE_URL" 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

2
server/service/auth/errors.ts

@ -27,7 +27,7 @@ export function toPublicAuthError(err: unknown) {
statusMessage: err.message, statusMessage: err.message,
}); });
} }
return createError({ return createError(err instanceof Error ? err : {
statusCode: 500, statusCode: 500,
statusMessage: "服务器繁忙,请稍后重试", statusMessage: "服务器繁忙,请稍后重试",
}); });

46
server/service/auth/index.ts

@ -65,17 +65,6 @@ function authFailedError() {
return new 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) { async function createSession(userId: number) {
const sessionId = randomUUID(); const sessionId = randomUUID();
const expiresAt = new Date(Date.now() + SESSION_EXPIRE_MS); const expiresAt = new Date(Date.now() + SESSION_EXPIRE_MS);
@ -96,37 +85,6 @@ async function getNextUserId() {
return (row?.maxId ?? 0) + 1; 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<MinimalUser> { async function insertUserWithRetry(username: string, passwordHash: string): Promise<MinimalUser> {
const maxRetry = 5; const maxRetry = 5;
for (let attempt = 0; attempt < maxRetry; attempt++) { for (let attempt = 0; attempt < maxRetry; attempt++) {
@ -145,10 +103,10 @@ async function insertUserWithRetry(username: string, passwordHash: string): Prom
}); });
return newUser as MinimalUser; return newUser as MinimalUser;
} catch (err) { } catch (err) {
if (isUsernameConflict(err)) { if (isUniqueConflictOnField(err, "username")) {
throw new AuthConflictError("用户名已存在"); throw new AuthConflictError("用户名已存在");
} }
if (isUserIdConflict(err) && attempt < maxRetry - 1) { if (isUniqueConflictExceptField(err, "username") && attempt < maxRetry - 1) {
continue; continue;
} }
throw err; throw err;

2
server/service/config/errors.ts

@ -23,7 +23,7 @@ export function toPublicConfigError(err: unknown) {
data: { code: err.code }, data: { code: err.code },
}); });
} }
return createError({ return createError(err instanceof Error ? err : {
statusCode: 500, statusCode: 500,
statusMessage: "服务器繁忙,请稍后重试", statusMessage: "服务器繁忙,请稍后重试",
}); });

82
server/utils/db-unique-constraint.ts

@ -0,0 +1,82 @@
/**
* / service
* vs {@link uniqueConstraintTouchesField}
*/
/** 遍历包装错误(如 Drizzle)与底层驱动的整条 cause 链 */
export function* eachErrorInChain(err: unknown): Generator<Error> {
let current: unknown = err;
const seen = new Set<unknown>();
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);
}
Loading…
Cancel
Save