aaa
This commit is contained in:
202
src/lib/db/schema.ts
Normal file
202
src/lib/db/schema.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
import {
|
||||
boolean,
|
||||
date,
|
||||
doublePrecision,
|
||||
index,
|
||||
integer,
|
||||
numeric,
|
||||
pgEnum,
|
||||
pgTable,
|
||||
primaryKey,
|
||||
serial,
|
||||
text,
|
||||
timestamp,
|
||||
uniqueIndex,
|
||||
} from "drizzle-orm/pg-core";
|
||||
|
||||
export const categoryEnum = pgEnum("category", [
|
||||
"food",
|
||||
"cafe",
|
||||
"shopping",
|
||||
"entertainment",
|
||||
"other",
|
||||
]);
|
||||
export const collectionTypeEnum = pgEnum("collection_type", ["folder", "trip"]);
|
||||
export const memberRoleEnum = pgEnum("member_role", ["owner", "editor", "viewer"]);
|
||||
export const priceRangeEnum = pgEnum("price_range", ["$", "$$", "$$$"]);
|
||||
|
||||
export const users = pgTable("users", {
|
||||
id: serial("id").primaryKey(),
|
||||
email: text("email").notNull().unique(),
|
||||
passwordHash: text("password_hash").notNull(),
|
||||
name: text("name").notNull(),
|
||||
initials: text("initials").notNull(),
|
||||
avatarUrl: text("avatar_url"),
|
||||
color: text("color"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
|
||||
// Session token is a cryptographic secret — must stay random text.
|
||||
// Sequential ints would be guessable and enable account takeover.
|
||||
export const sessions = pgTable(
|
||||
"sessions",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
userId: integer("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
|
||||
userAgent: text("user_agent"),
|
||||
},
|
||||
(t) => [
|
||||
index("sessions_user_id_idx").on(t.userId),
|
||||
index("sessions_expires_at_idx").on(t.expiresAt),
|
||||
],
|
||||
);
|
||||
|
||||
export const places = pgTable(
|
||||
"places",
|
||||
{
|
||||
id: serial("id").primaryKey(),
|
||||
createdBy: integer("created_by")
|
||||
.notNull()
|
||||
.references(() => users.id),
|
||||
name: text("name").notNull(),
|
||||
address: text("address").notNull(),
|
||||
shortAddress: text("short_address").notNull(),
|
||||
city: text("city").notNull(),
|
||||
lat: doublePrecision("lat"),
|
||||
lng: doublePrecision("lng"),
|
||||
category: categoryEnum("category").notNull(),
|
||||
tags: text("tags").array().notNull().default([]),
|
||||
coverUrl: text("cover_url"),
|
||||
phone: text("phone"),
|
||||
website: text("website"),
|
||||
priceRange: priceRangeEnum("price_range"),
|
||||
openingHours: text("opening_hours"),
|
||||
permanentlyClosed: boolean("permanently_closed").notNull().default(false),
|
||||
avgRating: numeric("avg_rating", { precision: 3, scale: 2 }),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(t) => [
|
||||
index("places_created_by_idx").on(t.createdBy),
|
||||
index("places_category_idx").on(t.category),
|
||||
],
|
||||
);
|
||||
|
||||
export const userPlaceData = pgTable(
|
||||
"user_place_data",
|
||||
{
|
||||
userId: integer("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id),
|
||||
placeId: integer("place_id")
|
||||
.notNull()
|
||||
.references(() => places.id, { onDelete: "cascade" }),
|
||||
notes: text("notes"),
|
||||
rating: integer("rating"),
|
||||
visited: boolean("visited").notNull().default(false),
|
||||
visitedAt: timestamp("visited_at", { withTimezone: true }),
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.userId, t.placeId] })],
|
||||
);
|
||||
|
||||
export const collections = pgTable("collections", {
|
||||
id: serial("id").primaryKey(),
|
||||
ownerId: integer("owner_id")
|
||||
.notNull()
|
||||
.references(() => users.id),
|
||||
name: text("name").notNull(),
|
||||
type: collectionTypeEnum("type").notNull(),
|
||||
tripStart: date("trip_start"),
|
||||
tripEnd: date("trip_end"),
|
||||
inviteToken: text("invite_token").unique(),
|
||||
tokenExpiresAt: timestamp("token_expires_at", { withTimezone: true }),
|
||||
publicToken: text("public_token").unique(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const collectionMembers = pgTable(
|
||||
"collection_members",
|
||||
{
|
||||
collectionId: integer("collection_id")
|
||||
.notNull()
|
||||
.references(() => collections.id, { onDelete: "cascade" }),
|
||||
userId: integer("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id),
|
||||
role: memberRoleEnum("role").notNull(),
|
||||
joinedAt: timestamp("joined_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.collectionId, t.userId] })],
|
||||
);
|
||||
|
||||
export const collectionPlaces = pgTable(
|
||||
"collection_places",
|
||||
{
|
||||
collectionId: integer("collection_id")
|
||||
.notNull()
|
||||
.references(() => collections.id, { onDelete: "cascade" }),
|
||||
placeId: integer("place_id")
|
||||
.notNull()
|
||||
.references(() => places.id, { onDelete: "cascade" }),
|
||||
addedBy: integer("added_by")
|
||||
.notNull()
|
||||
.references(() => users.id),
|
||||
sortOrder: integer("sort_order").notNull().default(0),
|
||||
addedAt: timestamp("added_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(t) => [
|
||||
primaryKey({ columns: [t.collectionId, t.placeId] }),
|
||||
index("collection_places_place_idx").on(t.placeId),
|
||||
],
|
||||
);
|
||||
|
||||
// Email-targeted invitation — one per (collection, email). Token is the
|
||||
// shareable secret; once accepted, the user joins the collection at `role`.
|
||||
export const invitations = pgTable(
|
||||
"invitations",
|
||||
{
|
||||
id: serial("id").primaryKey(),
|
||||
collectionId: integer("collection_id")
|
||||
.notNull()
|
||||
.references(() => collections.id, { onDelete: "cascade" }),
|
||||
email: text("email").notNull(),
|
||||
role: memberRoleEnum("role").notNull(),
|
||||
token: text("token").notNull().unique(),
|
||||
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
|
||||
invitedBy: integer("invited_by")
|
||||
.notNull()
|
||||
.references(() => users.id),
|
||||
acceptedAt: timestamp("accepted_at", { withTimezone: true }),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(t) => [
|
||||
uniqueIndex("invitations_collection_email_idx").on(t.collectionId, t.email),
|
||||
index("invitations_token_idx").on(t.token),
|
||||
],
|
||||
);
|
||||
|
||||
export const placeReviews = pgTable(
|
||||
"place_reviews",
|
||||
{
|
||||
id: serial("id").primaryKey(),
|
||||
placeId: integer("place_id")
|
||||
.notNull()
|
||||
.references(() => places.id, { onDelete: "cascade" }),
|
||||
collectionId: integer("collection_id")
|
||||
.notNull()
|
||||
.references(() => collections.id, { onDelete: "cascade" }),
|
||||
userId: integer("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id),
|
||||
body: text("body").notNull(),
|
||||
rating: integer("rating"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(t) => [
|
||||
uniqueIndex("place_reviews_unique").on(t.placeId, t.collectionId, t.userId),
|
||||
],
|
||||
);
|
||||
Reference in New Issue
Block a user