From 63830f249e3b814976e0d90d96fe31cb0dff3ead Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Tue, 20 Oct 2026 19:17:16 +0800 Subject: [PATCH] =?UTF-8?q?feat(db):=200000=20=E7=AB=99=E7=82=B9=E5=9F=9F?= =?UTF-8?q?=E2=80=94=E2=80=94sites/site=5Fconfig=5Frevisions=20+=20?= =?UTF-8?q?=E5=94=AF=E4=B8=80=E7=BA=A6=E6=9D=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit drizzle-kit 首个迁移从 0000 起编号(计划文案写 0001,以实际产出为准)。 迁移文件 + meta/_journal.json + snapshot 一并入库。 Co-Authored-By: Claude Code --- apps/api/drizzle/0000_pale_sumo.sql | 22 +++ apps/api/drizzle/meta/0000_snapshot.json | 172 +++++++++++++++++++++++ apps/api/drizzle/meta/_journal.json | 13 ++ apps/api/src/db/schema.ts | 23 ++- tests/integration/db.sites.test.ts | 26 ++++ 5 files changed, 254 insertions(+), 2 deletions(-) create mode 100644 apps/api/drizzle/0000_pale_sumo.sql create mode 100644 apps/api/drizzle/meta/0000_snapshot.json create mode 100644 apps/api/drizzle/meta/_journal.json create mode 100644 tests/integration/db.sites.test.ts diff --git a/apps/api/drizzle/0000_pale_sumo.sql b/apps/api/drizzle/0000_pale_sumo.sql new file mode 100644 index 0000000..b9032eb --- /dev/null +++ b/apps/api/drizzle/0000_pale_sumo.sql @@ -0,0 +1,22 @@ +CREATE TABLE "site_config_revisions" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "site_id" uuid NOT NULL, + "version" integer NOT NULL, + "data" jsonb NOT NULL, + "checksum" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "sites" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "key" text NOT NULL, + "name" text NOT NULL, + "default_locale" text NOT NULL, + "production_origin" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "sites_key_unique" UNIQUE("key") +); +--> statement-breakpoint +ALTER TABLE "site_config_revisions" ADD CONSTRAINT "site_config_revisions_site_id_sites_id_fk" FOREIGN KEY ("site_id") REFERENCES "public"."sites"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "site_config_rev_uq" ON "site_config_revisions" USING btree ("site_id","version"); \ No newline at end of file diff --git a/apps/api/drizzle/meta/0000_snapshot.json b/apps/api/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..0a40dc8 --- /dev/null +++ b/apps/api/drizzle/meta/0000_snapshot.json @@ -0,0 +1,172 @@ +{ + "id": "b744b7d5-e0a8-4940-ab52-2c0b46abbb50", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.site_config_revisions": { + "name": "site_config_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "site_id": { + "name": "site_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "site_config_rev_uq": { + "name": "site_config_rev_uq", + "columns": [ + { + "expression": "site_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "site_config_revisions_site_id_sites_id_fk": { + "name": "site_config_revisions_site_id_sites_id_fk", + "tableFrom": "site_config_revisions", + "tableTo": "sites", + "columnsFrom": [ + "site_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sites": { + "name": "sites", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "default_locale": { + "name": "default_locale", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "production_origin": { + "name": "production_origin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sites_key_unique": { + "name": "sites_key_unique", + "nullsNotDistinct": false, + "columns": [ + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/api/drizzle/meta/_journal.json b/apps/api/drizzle/meta/_journal.json new file mode 100644 index 0000000..607c215 --- /dev/null +++ b/apps/api/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1792494963444, + "tag": "0000_pale_sumo", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/apps/api/src/db/schema.ts b/apps/api/src/db/schema.ts index 258792a..e78bc8e 100644 --- a/apps/api/src/db/schema.ts +++ b/apps/api/src/db/schema.ts @@ -1,2 +1,21 @@ -// 聚合导出:后续任务(6-12)按域追加 pgTable 定义 -export {} +// 聚合导出:Task 6 起按域追加 pgTable 定义(迁移 0001——站点域) +import { pgTable, uuid, text, integer, jsonb, timestamp, uniqueIndex } from 'drizzle-orm/pg-core' + +export const sites = pgTable('sites', { + id: uuid('id').primaryKey().defaultRandom(), + key: text('key').notNull().unique(), + name: text('name').notNull(), + defaultLocale: text('default_locale').notNull(), + productionOrigin: text('production_origin').notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), +}) + +export const siteConfigRevisions = pgTable('site_config_revisions', { + id: uuid('id').primaryKey().defaultRandom(), + siteId: uuid('site_id').notNull().references(() => sites.id), + version: integer('version').notNull(), + data: jsonb('data').notNull(), + checksum: text('checksum').notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), +}, (t) => [uniqueIndex('site_config_rev_uq').on(t.siteId, t.version)]) diff --git a/tests/integration/db.sites.test.ts b/tests/integration/db.sites.test.ts new file mode 100644 index 0000000..599e977 --- /dev/null +++ b/tests/integration/db.sites.test.ts @@ -0,0 +1,26 @@ +// 站点域集成测试:约束在真 PG 上生效(唯一键、复合唯一键)。 +// 简易内联版——Task 13 收敛为公共 helper;本任务只需连库即可(getDb 从 apps/api 源码导入)。 +// 生命周期纪律:随机 key 插入 + finally 清理,可重复运行不残留脏数据。 +import { describe, it, expect } from 'vitest' +import { getDb } from '../../apps/api/src/db/client.js' + +describe('站点域约束', () => { + it('sites.key 唯一;site_config_revisions (site_id,version) 唯一', async () => { + const db = getDb() + const key = `fixture-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` + const r = (await db.execute(`insert into sites (id, key, name, default_locale, production_origin) + values (gen_random_uuid(), '${key}', 'Fixture', 'en', 'https://fixture.example') returning id`)) as any + const siteId = r[0].id + try { + await expect(db.execute(`insert into sites (id, key, name, default_locale, production_origin) + values (gen_random_uuid(), '${key}', 'Dup', 'en', 'https://x.example')`)).rejects.toThrow() + await db.execute(`insert into site_config_revisions (id, site_id, version, data, checksum) + values (gen_random_uuid(), '${siteId}', 1, '{"a":1}', 'c1')`) + await expect(db.execute(`insert into site_config_revisions (id, site_id, version, data, checksum) + values (gen_random_uuid(), '${siteId}', 1, '{"a":2}', 'c2')`)).rejects.toThrow() + } finally { + await db.execute(`delete from site_config_revisions where site_id = '${siteId}'`) + await db.execute(`delete from sites where id = '${siteId}'`) + } + }) +})