- 10 test cases covering search, owner count, link/unlink, seed idempotency - Added globalItems/itemGlobalLinks tables to SQLite schema - Added Zod schemas and types for global items - Created 18-item bikepacking gear seed data JSON
189 lines
6.4 KiB
TypeScript
189 lines
6.4 KiB
TypeScript
import { integer, real, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
|
|
export const categories = sqliteTable("categories", {
|
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
name: text("name").notNull().unique(),
|
|
icon: text("icon").notNull().default("package"),
|
|
createdAt: integer("created_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.$defaultFn(() => new Date()),
|
|
});
|
|
|
|
export const items = sqliteTable("items", {
|
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
name: text("name").notNull(),
|
|
weightGrams: real("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"),
|
|
quantity: integer("quantity").notNull().default(1),
|
|
createdAt: integer("created_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.$defaultFn(() => new Date()),
|
|
updatedAt: integer("updated_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.$defaultFn(() => new Date()),
|
|
});
|
|
|
|
export const threads = sqliteTable("threads", {
|
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
name: text("name").notNull(),
|
|
status: text("status").notNull().default("active"),
|
|
resolvedCandidateId: integer("resolved_candidate_id"),
|
|
categoryId: integer("category_id")
|
|
.notNull()
|
|
.references(() => categories.id),
|
|
createdAt: integer("created_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.$defaultFn(() => new Date()),
|
|
updatedAt: integer("updated_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.$defaultFn(() => new Date()),
|
|
});
|
|
|
|
export const threadCandidates = sqliteTable("thread_candidates", {
|
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
threadId: integer("thread_id")
|
|
.notNull()
|
|
.references(() => threads.id, { onDelete: "cascade" }),
|
|
name: text("name").notNull(),
|
|
weightGrams: real("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: real("sort_order").notNull().default(0),
|
|
createdAt: integer("created_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.$defaultFn(() => new Date()),
|
|
updatedAt: integer("updated_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.$defaultFn(() => new Date()),
|
|
});
|
|
|
|
export const setups = sqliteTable("setups", {
|
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
name: text("name").notNull(),
|
|
createdAt: integer("created_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.$defaultFn(() => new Date()),
|
|
updatedAt: integer("updated_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.$defaultFn(() => new Date()),
|
|
});
|
|
|
|
export const setupItems = sqliteTable("setup_items", {
|
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
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"),
|
|
});
|
|
|
|
export const settings = sqliteTable("settings", {
|
|
key: text("key").primaryKey(),
|
|
value: text("value").notNull(),
|
|
});
|
|
|
|
export const users = sqliteTable("users", {
|
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
username: text("username").notNull().unique(),
|
|
passwordHash: text("password_hash").notNull(),
|
|
createdAt: integer("created_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.$defaultFn(() => new Date()),
|
|
});
|
|
|
|
export const sessions = sqliteTable("sessions", {
|
|
id: text("id").primaryKey(),
|
|
userId: integer("user_id")
|
|
.notNull()
|
|
.references(() => users.id, { onDelete: "cascade" }),
|
|
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
|
|
});
|
|
|
|
export const apiKeys = sqliteTable("api_keys", {
|
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
name: text("name").notNull(),
|
|
keyHash: text("key_hash").notNull(),
|
|
keyPrefix: text("key_prefix").notNull(),
|
|
createdAt: integer("created_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.$defaultFn(() => new Date()),
|
|
});
|
|
|
|
export const globalItems = sqliteTable("global_items", {
|
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
brand: text("brand").notNull(),
|
|
model: text("model").notNull(),
|
|
category: text("category"),
|
|
weightGrams: real("weight_grams"),
|
|
priceCents: integer("price_cents"),
|
|
imageUrl: text("image_url"),
|
|
description: text("description"),
|
|
createdAt: integer("created_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.$defaultFn(() => new Date()),
|
|
});
|
|
|
|
export const itemGlobalLinks = sqliteTable("item_global_links", {
|
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
itemId: integer("item_id")
|
|
.notNull()
|
|
.references(() => items.id, { onDelete: "cascade" })
|
|
.unique(),
|
|
globalItemId: integer("global_item_id")
|
|
.notNull()
|
|
.references(() => globalItems.id, { onDelete: "cascade" }),
|
|
});
|
|
|
|
export const oauthClients = sqliteTable("oauth_clients", {
|
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
clientId: text("client_id").notNull().unique(),
|
|
clientName: text("client_name"),
|
|
redirectUris: text("redirect_uris").notNull(), // JSON array
|
|
createdAt: integer("created_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.$defaultFn(() => new Date()),
|
|
});
|
|
|
|
export const oauthCodes = sqliteTable("oauth_codes", {
|
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
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: integer("expires_at", { mode: "timestamp" }).notNull(),
|
|
used: integer("used").notNull().default(0),
|
|
});
|
|
|
|
export const oauthTokens = sqliteTable("oauth_tokens", {
|
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
accessTokenHash: text("access_token_hash").notNull().unique(),
|
|
refreshTokenHash: text("refresh_token_hash").notNull().unique(),
|
|
clientId: text("client_id").notNull(),
|
|
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), // access token expiry
|
|
refreshExpiresAt: integer("refresh_expires_at", {
|
|
mode: "timestamp",
|
|
}).notNull(), // refresh token expiry
|
|
createdAt: integer("created_at", { mode: "timestamp" })
|
|
.notNull()
|
|
.$defaultFn(() => new Date()),
|
|
});
|