- Add globalItemId and purchasePriceCents columns to items table - Add globalItemId column to threadCandidates table - Add tags and globalItemTags tables for tag system - Remove itemGlobalLinks table (replaced by direct FK) - Generate migration with data migration step before table drop
234 lines
9.0 KiB
TypeScript
234 lines
9.0 KiB
TypeScript
import {
|
|
boolean,
|
|
doublePrecision,
|
|
integer,
|
|
pgTable,
|
|
primaryKey,
|
|
serial,
|
|
text,
|
|
timestamp,
|
|
unique,
|
|
} from "drizzle-orm/pg-core";
|
|
|
|
// ── Users ───────────────────────────────────────────────────────────
|
|
|
|
export const users = pgTable("users", {
|
|
id: serial("id").primaryKey(),
|
|
logtoSub: text("logto_sub").notNull().unique(),
|
|
displayName: text("display_name"),
|
|
avatarUrl: text("avatar_url"),
|
|
bio: text("bio"),
|
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
});
|
|
|
|
// ── Categories ──────────────────────────────────────────────────────
|
|
|
|
export const categories = pgTable(
|
|
"categories",
|
|
{
|
|
id: serial("id").primaryKey(),
|
|
name: text("name").notNull(),
|
|
icon: text("icon").notNull().default("package"),
|
|
userId: integer("user_id")
|
|
.notNull()
|
|
.references(() => users.id),
|
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
},
|
|
(table) => [unique().on(table.userId, table.name)],
|
|
);
|
|
|
|
// ── Items ───────────────────────────────────────────────────────────
|
|
|
|
export const items = pgTable("items", {
|
|
id: serial("id").primaryKey(),
|
|
name: text("name").notNull(),
|
|
weightGrams: doublePrecision("weight_grams"),
|
|
priceCents: integer("price_cents"),
|
|
categoryId: integer("category_id")
|
|
.notNull()
|
|
.references(() => categories.id),
|
|
userId: integer("user_id")
|
|
.notNull()
|
|
.references(() => users.id),
|
|
notes: text("notes"),
|
|
productUrl: text("product_url"),
|
|
imageFilename: text("image_filename"),
|
|
imageSourceUrl: text("image_source_url"),
|
|
quantity: integer("quantity").notNull().default(1),
|
|
globalItemId: integer("global_item_id").references(() => globalItems.id),
|
|
purchasePriceCents: integer("purchase_price_cents"),
|
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
|
});
|
|
|
|
// ── Threads ─────────────────────────────────────────────────────────
|
|
|
|
export const threads = pgTable("threads", {
|
|
id: serial("id").primaryKey(),
|
|
name: text("name").notNull(),
|
|
status: text("status").notNull().default("active"),
|
|
resolvedCandidateId: integer("resolved_candidate_id"),
|
|
categoryId: integer("category_id")
|
|
.notNull()
|
|
.references(() => categories.id),
|
|
userId: integer("user_id")
|
|
.notNull()
|
|
.references(() => users.id),
|
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
|
});
|
|
|
|
// ── Thread Candidates ───────────────────────────────────────────────
|
|
|
|
export const threadCandidates = pgTable("thread_candidates", {
|
|
id: serial("id").primaryKey(),
|
|
threadId: integer("thread_id")
|
|
.notNull()
|
|
.references(() => threads.id, { onDelete: "cascade" }),
|
|
name: text("name").notNull(),
|
|
weightGrams: doublePrecision("weight_grams"),
|
|
priceCents: integer("price_cents"),
|
|
categoryId: integer("category_id")
|
|
.notNull()
|
|
.references(() => categories.id),
|
|
notes: text("notes"),
|
|
productUrl: text("product_url"),
|
|
imageFilename: text("image_filename"),
|
|
imageSourceUrl: text("image_source_url"),
|
|
status: text("status").notNull().default("researching"),
|
|
pros: text("pros"),
|
|
cons: text("cons"),
|
|
sortOrder: doublePrecision("sort_order").notNull().default(0),
|
|
globalItemId: integer("global_item_id").references(() => globalItems.id),
|
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
|
});
|
|
|
|
// ── Setups ──────────────────────────────────────────────────────────
|
|
|
|
export const setups = pgTable("setups", {
|
|
id: serial("id").primaryKey(),
|
|
name: text("name").notNull(),
|
|
userId: integer("user_id")
|
|
.notNull()
|
|
.references(() => users.id),
|
|
isPublic: boolean("is_public").notNull().default(false),
|
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
|
});
|
|
|
|
// ── Setup Items ─────────────────────────────────────────────────────
|
|
|
|
export const setupItems = pgTable("setup_items", {
|
|
id: serial("id").primaryKey(),
|
|
setupId: integer("setup_id")
|
|
.notNull()
|
|
.references(() => setups.id, { onDelete: "cascade" }),
|
|
itemId: integer("item_id")
|
|
.notNull()
|
|
.references(() => items.id, { onDelete: "cascade" }),
|
|
classification: text("classification").notNull().default("base"),
|
|
});
|
|
|
|
// ── Global Items ────────────────────────────────────────────────────
|
|
|
|
export const globalItems = pgTable("global_items", {
|
|
id: serial("id").primaryKey(),
|
|
brand: text("brand").notNull(),
|
|
model: text("model").notNull(),
|
|
category: text("category"),
|
|
weightGrams: doublePrecision("weight_grams"),
|
|
priceCents: integer("price_cents"),
|
|
imageUrl: text("image_url"),
|
|
description: text("description"),
|
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
});
|
|
|
|
// ── Tags ───────────────────────────────────────────────────────────
|
|
|
|
export const tags = pgTable("tags", {
|
|
id: serial("id").primaryKey(),
|
|
name: text("name").notNull().unique(),
|
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
});
|
|
|
|
// ── Global Item Tags ───────────────────────────────────────────────
|
|
|
|
export const globalItemTags = pgTable(
|
|
"global_item_tags",
|
|
{
|
|
globalItemId: integer("global_item_id")
|
|
.notNull()
|
|
.references(() => globalItems.id, { onDelete: "cascade" }),
|
|
tagId: integer("tag_id")
|
|
.notNull()
|
|
.references(() => tags.id, { onDelete: "cascade" }),
|
|
},
|
|
(table) => [primaryKey({ columns: [table.globalItemId, table.tagId] })],
|
|
);
|
|
|
|
// ── Settings ────────────────────────────────────────────────────────
|
|
|
|
export const settings = pgTable(
|
|
"settings",
|
|
{
|
|
userId: integer("user_id")
|
|
.notNull()
|
|
.references(() => users.id),
|
|
key: text("key").notNull(),
|
|
value: text("value").notNull(),
|
|
},
|
|
(table) => [primaryKey({ columns: [table.userId, table.key] })],
|
|
);
|
|
|
|
// ── API Keys ────────────────────────────────────────────────────────
|
|
|
|
export const apiKeys = pgTable("api_keys", {
|
|
id: serial("id").primaryKey(),
|
|
name: text("name").notNull(),
|
|
keyHash: text("key_hash").notNull(),
|
|
keyPrefix: text("key_prefix").notNull(),
|
|
userId: integer("user_id")
|
|
.notNull()
|
|
.references(() => users.id),
|
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
});
|
|
|
|
// ── OAuth Clients ───────────────────────────────────────────────────
|
|
|
|
export const oauthClients = pgTable("oauth_clients", {
|
|
id: serial("id").primaryKey(),
|
|
clientId: text("client_id").notNull().unique(),
|
|
clientName: text("client_name"),
|
|
redirectUris: text("redirect_uris").notNull(), // JSON array
|
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
});
|
|
|
|
// ── OAuth Authorization Codes ───────────────────────────────────────
|
|
|
|
export const oauthCodes = pgTable("oauth_codes", {
|
|
id: serial("id").primaryKey(),
|
|
code: text("code").notNull().unique(),
|
|
clientId: text("client_id").notNull(),
|
|
codeChallenge: text("code_challenge").notNull(),
|
|
codeChallengeMethod: text("code_challenge_method").notNull().default("S256"),
|
|
redirectUri: text("redirect_uri").notNull(),
|
|
expiresAt: timestamp("expires_at").notNull(),
|
|
used: integer("used").notNull().default(0),
|
|
});
|
|
|
|
// ── OAuth Tokens ────────────────────────────────────────────────────
|
|
|
|
export const oauthTokens = pgTable("oauth_tokens", {
|
|
id: serial("id").primaryKey(),
|
|
accessTokenHash: text("access_token_hash").notNull().unique(),
|
|
refreshTokenHash: text("refresh_token_hash").notNull().unique(),
|
|
clientId: text("client_id").notNull(),
|
|
userId: integer("user_id")
|
|
.notNull()
|
|
.references(() => users.id),
|
|
expiresAt: timestamp("expires_at").notNull(),
|
|
refreshExpiresAt: timestamp("refresh_expires_at").notNull(),
|
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
});
|