You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

195 lines
5.7 KiB

import {
sqliteTable,
text,
integer,
real,
primaryKey,
index,
uniqueIndex,
} from "drizzle-orm/sqlite-core";
import { users } from "./auth";
// ============ CardType ENUM ============
export const CardTypes = [
"text",
"image",
"image-text",
"portfolio",
"project",
] as const;
export type CardType = (typeof CardTypes)[number];
// ============ Category(分类表)============
export const categories = sqliteTable(
"categories",
{
id: text("id").primaryKey(), // UUID
name: text("name", { length: 100 }).notNull(),
slug: text("slug", { length: 100 }).notNull(),
image: text("image", { length: 500 }),
parentId: text("parent_id"), // 自引用 FK,NULL=顶级
sortOrder: integer("sort_order").default(0).notNull(),
count: integer("count").default(0).notNull(),
createdAt: integer("created_at", { mode: "timestamp_ms" })
.defaultNow()
.notNull(),
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
.defaultNow()
.$onUpdate(() => new Date())
.notNull(),
},
(table) => [
uniqueIndex("idx_category_slug").on(table.slug),
index("idx_category_parent").on(table.parentId),
],
);
// ============ Card(卡片表)============
export const cards = sqliteTable(
"cards",
{
id: integer("id").primaryKey({ autoIncrement: true }),
type: text("type", { enum: CardTypes }).notNull(),
title: text("title", { length: 255 }).notNull(),
description: text("description"),
aspectRatio: real("aspect_ratio"),
categoryId: text("category_id").references(() => categories.id),
createdAt: integer("created_at", { mode: "timestamp_ms" })
.defaultNow()
.notNull(),
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
.defaultNow()
.$onUpdate(() => new Date())
.notNull(),
},
(table) => [
index("idx_card_category").on(table.categoryId),
index("idx_card_type").on(table.type),
index("idx_card_created").on(table.createdAt),
],
);
// ============ CardImage(卡片图片表)============
export const cardImages = sqliteTable(
"card_images",
{
id: integer("id").primaryKey({ autoIncrement: true }),
cardId: integer("card_id")
.notNull()
.references(() => cards.id),
url: text("url", { length: 500 }).notNull(),
sortOrder: integer("sort_order").default(0).notNull(),
},
(table) => [index("idx_card_image_card").on(table.cardId)],
);
// ============ Tag(标签表)============
export const tags = sqliteTable(
"tags",
{
id: integer("id").primaryKey({ autoIncrement: true }),
name: text("name", { length: 50 }).notNull(),
slug: text("slug", { length: 50 }).notNull(),
},
(table) => [uniqueIndex("idx_tag_slug").on(table.slug)],
);
// ============ CardTag(卡片-标签关联表)============
export const cardTags = sqliteTable(
"card_tags",
{
cardId: integer("card_id")
.notNull()
.references(() => cards.id),
tagId: integer("tag_id")
.notNull()
.references(() => tags.id),
},
(table) => [
primaryKey({ name: "card_tag_pk", columns: [table.cardId, table.tagId] }),
index("idx_card_tag_card").on(table.cardId),
index("idx_card_tag_tag").on(table.tagId),
],
);
// ============ Favorite(收藏表)============
export const favorites = sqliteTable(
"favorites",
{
userId: integer("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
cardId: integer("card_id")
.notNull()
.references(() => cards.id, { onDelete: "cascade" }),
createdAt: integer("created_at", { mode: "timestamp_ms" })
.defaultNow()
.notNull(),
},
(table) => [
primaryKey({ name: "favorite_pk", columns: [table.userId, table.cardId] }),
index("idx_favorite_user").on(table.userId),
index("idx_favorite_card").on(table.cardId),
],
);
// ============ Tool(工具项表)============
export const tools = sqliteTable("tools", {
id: text("id").primaryKey(), // UUID
name: text("name", { length: 50 }).notNull(),
slug: text("slug", { length: 50 }).notNull(),
icon: text("icon", { length: 100 }),
sortOrder: integer("sort_order").default(0).notNull(),
});
// ============ ArticleStatus ENUM ============
export const ArticleStatuses = ["draft", "published"] as const;
export type ArticleStatus = (typeof ArticleStatuses)[number];
// ============ Article(文章表)============
export const articles = sqliteTable(
"articles",
{
id: integer("id").primaryKey({ autoIncrement: true }),
title: text("title", { length: 255 }).notNull(),
content: text("content").notNull(),
summary: text("summary"),
cover: text("cover", { length: 500 }),
status: text("status", { enum: ArticleStatuses })
.default("draft")
.notNull(),
createdAt: integer("created_at", { mode: "timestamp_ms" })
.defaultNow()
.notNull(),
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
.defaultNow()
.$onUpdate(() => new Date())
.notNull(),
},
(table) => [
index("idx_article_status").on(table.status),
index("idx_article_created").on(table.createdAt),
],
);
// ============ ArticleCard(文章-卡片关联表)============
export const articleCards = sqliteTable(
"article_cards",
{
articleId: integer("article_id")
.notNull()
.references(() => articles.id, { onDelete: "cascade" }),
cardId: integer("card_id")
.notNull()
.references(() => cards.id, { onDelete: "cascade" }),
sortOrder: integer("sort_order").default(0).notNull(),
},
(table) => [
primaryKey({
name: "article_card_pk",
columns: [table.articleId, table.cardId],
}),
index("idx_article_card_article").on(table.articleId),
index("idx_article_card_card").on(table.cardId),
],
);