Initial commit of Vellum, an event photo product for guest uploads, host moderation, and original-quality galleries.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { pgTable, text, timestamp, boolean, index } from "drizzle-orm/pg-core";
|
||||
|
||||
export const user = pgTable("user", {
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
email: text("email").notNull().unique(),
|
||||
emailVerified: boolean("email_verified").default(false).notNull(),
|
||||
image: text("image"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true })
|
||||
.defaultNow()
|
||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||
.notNull(),
|
||||
});
|
||||
|
||||
export const session = pgTable(
|
||||
"session",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
|
||||
token: text("token").notNull().unique(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true })
|
||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||
.notNull(),
|
||||
ipAddress: text("ip_address"),
|
||||
userAgent: text("user_agent"),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
},
|
||||
(table) => [index("session_userId_idx").on(table.userId)],
|
||||
);
|
||||
|
||||
export const account = pgTable(
|
||||
"account",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
accountId: text("account_id").notNull(),
|
||||
providerId: text("provider_id").notNull(),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
accessToken: text("access_token"),
|
||||
refreshToken: text("refresh_token"),
|
||||
idToken: text("id_token"),
|
||||
accessTokenExpiresAt: timestamp("access_token_expires_at", {
|
||||
withTimezone: true,
|
||||
}),
|
||||
refreshTokenExpiresAt: timestamp("refresh_token_expires_at", {
|
||||
withTimezone: true,
|
||||
}),
|
||||
scope: text("scope"),
|
||||
password: text("password"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true })
|
||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||
.notNull(),
|
||||
},
|
||||
(table) => [index("account_userId_idx").on(table.userId)],
|
||||
);
|
||||
|
||||
export const verification = pgTable(
|
||||
"verification",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
identifier: text("identifier").notNull(),
|
||||
value: text("value").notNull(),
|
||||
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true })
|
||||
.defaultNow()
|
||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||
.notNull(),
|
||||
},
|
||||
(table) => [index("verification_identifier_idx").on(table.identifier)],
|
||||
);
|
||||
|
||||
export const userRelations = relations(user, ({ many }) => ({
|
||||
sessions: many(session),
|
||||
accounts: many(account),
|
||||
}));
|
||||
|
||||
export const sessionRelations = relations(session, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [session.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const accountRelations = relations(account, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [account.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
@@ -0,0 +1,56 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { config } from "dotenv";
|
||||
import { drizzle } from "drizzle-orm/postgres-js";
|
||||
import postgres from "postgres";
|
||||
import * as appSchema from "./schema";
|
||||
import * as authSchema from "./auth-schema";
|
||||
|
||||
for (const path of [
|
||||
resolve(process.cwd(), ".env"),
|
||||
resolve(process.cwd(), "../../.env"),
|
||||
]) {
|
||||
if (existsSync(path)) {
|
||||
config({ path, override: false });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const schema = { ...appSchema, ...authSchema };
|
||||
|
||||
type PostgresClient = ReturnType<typeof postgres>;
|
||||
type Database = ReturnType<typeof drizzle<typeof schema>>;
|
||||
export type { Database };
|
||||
const globalDatabase = globalThis as typeof globalThis & {
|
||||
albumPostgresClient?: PostgresClient;
|
||||
albumDatabase?: Database;
|
||||
};
|
||||
|
||||
let client = globalDatabase.albumPostgresClient;
|
||||
let database = globalDatabase.albumDatabase;
|
||||
|
||||
export function getDb() {
|
||||
if (database) return database;
|
||||
const url = process.env.DATABASE_URL;
|
||||
if (!url) {
|
||||
throw new Error("DATABASE_URL is required for database operations");
|
||||
}
|
||||
client = postgres(url, {
|
||||
max: process.env.NODE_ENV === "production" ? 10 : 3,
|
||||
idle_timeout: 20,
|
||||
});
|
||||
database = drizzle(client, { schema });
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
globalDatabase.albumPostgresClient = client;
|
||||
globalDatabase.albumDatabase = database;
|
||||
}
|
||||
return database;
|
||||
}
|
||||
|
||||
export async function closeDb() {
|
||||
await client?.end();
|
||||
client = undefined;
|
||||
database = undefined;
|
||||
globalDatabase.albumPostgresClient = undefined;
|
||||
globalDatabase.albumDatabase = undefined;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { closeDb, getDb, type Database } from "./db";
|
||||
export * from "./schema";
|
||||
export * from "./auth-schema";
|
||||
@@ -0,0 +1,16 @@
|
||||
import "dotenv/config";
|
||||
import { drizzle } from "drizzle-orm/postgres-js";
|
||||
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
||||
import postgres from "postgres";
|
||||
|
||||
const url =
|
||||
process.env.DATABASE_URL ??
|
||||
"postgres://album:album@localhost:5439/album";
|
||||
|
||||
const client = postgres(url, { max: 1 });
|
||||
const db = drizzle(client);
|
||||
|
||||
await migrate(db, { migrationsFolder: "./drizzle" });
|
||||
await client.end();
|
||||
|
||||
console.log("Migrations applied successfully.");
|
||||
@@ -0,0 +1,491 @@
|
||||
import {
|
||||
boolean,
|
||||
index,
|
||||
integer,
|
||||
jsonb,
|
||||
pgEnum,
|
||||
pgTable,
|
||||
text,
|
||||
timestamp,
|
||||
uniqueIndex,
|
||||
uuid,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { relations } from "drizzle-orm";
|
||||
import { user } from "./auth-schema";
|
||||
|
||||
const timestamps = {
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
};
|
||||
|
||||
export const eventStatus = pgEnum("event_status", [
|
||||
"draft",
|
||||
"published",
|
||||
"closed",
|
||||
]);
|
||||
|
||||
export const groupRole = pgEnum("group_role", ["owner", "member"]);
|
||||
|
||||
export const eventRole = pgEnum("event_role", [
|
||||
"owner",
|
||||
"manager",
|
||||
"moderator",
|
||||
"viewer",
|
||||
]);
|
||||
|
||||
export const platformRole = pgEnum("platform_role", [
|
||||
"super_admin",
|
||||
"admin",
|
||||
"moderator",
|
||||
"viewer",
|
||||
]);
|
||||
|
||||
export const eventCreatePolicy = pgEnum("event_create_policy", [
|
||||
"open",
|
||||
"invite",
|
||||
"admin_only",
|
||||
]);
|
||||
|
||||
export const entitlementSource = pgEnum("entitlement_source", [
|
||||
"signup_default",
|
||||
"invite",
|
||||
"code",
|
||||
"platform",
|
||||
]);
|
||||
|
||||
export const inviteKind = pgEnum("invite_kind", ["email", "code"]);
|
||||
|
||||
export const inviteStatus = pgEnum("invite_status", [
|
||||
"pending",
|
||||
"accepted",
|
||||
"revoked",
|
||||
"expired",
|
||||
]);
|
||||
|
||||
export const photoProcessingStatus = pgEnum("photo_processing_status", [
|
||||
"uploading",
|
||||
"processing",
|
||||
"ready",
|
||||
"failed",
|
||||
]);
|
||||
|
||||
export const photoVisibility = pgEnum("photo_visibility", [
|
||||
"pending",
|
||||
"public",
|
||||
"hidden",
|
||||
"private",
|
||||
"rejected",
|
||||
]);
|
||||
|
||||
export const photoJobStatus = pgEnum("photo_job_status", [
|
||||
"pending",
|
||||
"processing",
|
||||
"completed",
|
||||
"failed",
|
||||
]);
|
||||
|
||||
export const groups = pgTable(
|
||||
"groups",
|
||||
{
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
slug: text("slug").notNull(),
|
||||
createdByUserId: text("created_by_user_id").references(() => user.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
...timestamps,
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex("groups_slug_idx").on(table.slug),
|
||||
index("groups_created_by_user_id_idx").on(table.createdByUserId),
|
||||
],
|
||||
);
|
||||
|
||||
export const groupMemberships = pgTable(
|
||||
"group_memberships",
|
||||
{
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
groupId: uuid("group_id")
|
||||
.notNull()
|
||||
.references(() => groups.id, { onDelete: "cascade" }),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
role: groupRole("role").notNull().default("member"),
|
||||
...timestamps,
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex("group_memberships_group_user_idx").on(
|
||||
table.groupId,
|
||||
table.userId,
|
||||
),
|
||||
index("group_memberships_user_idx").on(table.userId),
|
||||
],
|
||||
);
|
||||
|
||||
export const events = pgTable(
|
||||
"events",
|
||||
{
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
groupId: uuid("group_id")
|
||||
.notNull()
|
||||
.references(() => groups.id, { onDelete: "cascade" }),
|
||||
slug: text("slug").notNull(),
|
||||
title: text("title").notNull(),
|
||||
description: text("description"),
|
||||
startsAt: timestamp("starts_at", { withTimezone: true }),
|
||||
endsAt: timestamp("ends_at", { withTimezone: true }),
|
||||
status: eventStatus("status").notNull().default("draft"),
|
||||
listed: boolean("listed").notNull().default(false),
|
||||
uploadEnabled: boolean("upload_enabled").notNull().default(true),
|
||||
galleryReleasedAt: timestamp("gallery_released_at", { withTimezone: true }),
|
||||
...timestamps,
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex("events_slug_idx").on(table.slug),
|
||||
index("events_group_id_idx").on(table.groupId),
|
||||
index("events_listed_idx").on(table.listed, table.status),
|
||||
],
|
||||
);
|
||||
|
||||
export const eventMemberships = pgTable(
|
||||
"event_memberships",
|
||||
{
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
eventId: uuid("event_id")
|
||||
.notNull()
|
||||
.references(() => events.id, { onDelete: "cascade" }),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
role: eventRole("role").notNull().default("viewer"),
|
||||
...timestamps,
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex("event_memberships_event_user_idx").on(
|
||||
table.eventId,
|
||||
table.userId,
|
||||
),
|
||||
index("event_memberships_user_idx").on(table.userId),
|
||||
],
|
||||
);
|
||||
|
||||
export const platformAdministrators = pgTable(
|
||||
"platform_administrators",
|
||||
{
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
role: platformRole("role").notNull().default("admin"),
|
||||
...timestamps,
|
||||
},
|
||||
(table) => [uniqueIndex("platform_administrators_user_idx").on(table.userId)],
|
||||
);
|
||||
|
||||
export const deploymentSettings = pgTable("deployment_settings", {
|
||||
id: text("id").primaryKey().default("default"),
|
||||
openSignup: boolean("open_signup").notNull().default(true),
|
||||
eventCreatePolicy: eventCreatePolicy("event_create_policy")
|
||||
.notNull()
|
||||
.default("open"),
|
||||
defaultEventLimit: integer("default_event_limit").notNull().default(1),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
});
|
||||
|
||||
export const entitlements = pgTable(
|
||||
"entitlements",
|
||||
{
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
groupId: uuid("group_id")
|
||||
.notNull()
|
||||
.references(() => groups.id, { onDelete: "cascade" }),
|
||||
eventLimit: integer("event_limit"),
|
||||
expiresAt: timestamp("expires_at", { withTimezone: true }),
|
||||
complimentary: boolean("complimentary").notNull().default(false),
|
||||
source: entitlementSource("source").notNull().default("signup_default"),
|
||||
grantedByUserId: text("granted_by_user_id").references(() => user.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
...timestamps,
|
||||
},
|
||||
(table) => [index("entitlements_group_id_idx").on(table.groupId)],
|
||||
);
|
||||
|
||||
export const invites = pgTable(
|
||||
"invites",
|
||||
{
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
kind: inviteKind("kind").notNull(),
|
||||
status: inviteStatus("status").notNull().default("pending"),
|
||||
email: text("email"),
|
||||
tokenHash: text("token_hash").notNull(),
|
||||
reusable: boolean("reusable").notNull().default(false),
|
||||
maxUses: integer("max_uses").notNull().default(1),
|
||||
usedCount: integer("used_count").notNull().default(0),
|
||||
groupId: uuid("group_id").references(() => groups.id, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
eventId: uuid("event_id").references(() => events.id, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
groupRole: groupRole("group_role"),
|
||||
eventRole: eventRole("event_role"),
|
||||
grantUnlimitedEvents: boolean("grant_unlimited_events")
|
||||
.notNull()
|
||||
.default(false),
|
||||
grantEventLimit: integer("grant_event_limit"),
|
||||
grantComplimentary: boolean("grant_complimentary").notNull().default(false),
|
||||
createdByUserId: text("created_by_user_id").references(() => user.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
acceptedByUserId: text("accepted_by_user_id").references(() => user.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
expiresAt: timestamp("expires_at", { withTimezone: true }),
|
||||
acceptedAt: timestamp("accepted_at", { withTimezone: true }),
|
||||
...timestamps,
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex("invites_token_hash_idx").on(table.tokenHash),
|
||||
index("invites_email_idx").on(table.email),
|
||||
index("invites_group_id_idx").on(table.groupId),
|
||||
index("invites_event_id_idx").on(table.eventId),
|
||||
],
|
||||
);
|
||||
|
||||
export const guests = pgTable(
|
||||
"guests",
|
||||
{
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
eventId: uuid("event_id")
|
||||
.notNull()
|
||||
.references(() => events.id, { onDelete: "cascade" }),
|
||||
displayName: text("display_name"),
|
||||
email: text("email"),
|
||||
notifyWhenReady: boolean("notify_when_ready").notNull().default(false),
|
||||
notifiedAt: timestamp("notified_at", { withTimezone: true }),
|
||||
note: text("note"),
|
||||
tokenHash: text("token_hash").notNull(),
|
||||
...timestamps,
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex("guests_event_token_idx").on(table.eventId, table.tokenHash),
|
||||
index("guests_event_id_idx").on(table.eventId),
|
||||
],
|
||||
);
|
||||
|
||||
export const submissions = pgTable(
|
||||
"submissions",
|
||||
{
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
eventId: uuid("event_id")
|
||||
.notNull()
|
||||
.references(() => events.id, { onDelete: "cascade" }),
|
||||
guestId: uuid("guest_id")
|
||||
.notNull()
|
||||
.references(() => guests.id, { onDelete: "cascade" }),
|
||||
...timestamps,
|
||||
},
|
||||
(table) => [
|
||||
index("submissions_event_id_idx").on(table.eventId),
|
||||
index("submissions_guest_id_idx").on(table.guestId),
|
||||
],
|
||||
);
|
||||
|
||||
export const photos = pgTable(
|
||||
"photos",
|
||||
{
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
eventId: uuid("event_id")
|
||||
.notNull()
|
||||
.references(() => events.id, { onDelete: "cascade" }),
|
||||
submissionId: uuid("submission_id")
|
||||
.notNull()
|
||||
.references(() => submissions.id, { onDelete: "cascade" }),
|
||||
processingStatus: photoProcessingStatus("processing_status")
|
||||
.notNull()
|
||||
.default("uploading"),
|
||||
visibility: photoVisibility("visibility").notNull().default("pending"),
|
||||
originalKey: text("original_key").notNull(),
|
||||
displayKey: text("display_key"),
|
||||
thumbKey: text("thumb_key"),
|
||||
contentType: text("content_type").notNull(),
|
||||
byteSize: integer("byte_size").notNull().default(0),
|
||||
width: integer("width"),
|
||||
height: integer("height"),
|
||||
...timestamps,
|
||||
},
|
||||
(table) => [
|
||||
index("photos_event_id_idx").on(table.eventId),
|
||||
index("photos_submission_id_idx").on(table.submissionId),
|
||||
index("photos_event_visibility_idx").on(table.eventId, table.visibility),
|
||||
],
|
||||
);
|
||||
|
||||
export const photoJobs = pgTable(
|
||||
"photo_jobs",
|
||||
{
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
photoId: uuid("photo_id")
|
||||
.notNull()
|
||||
.references(() => photos.id, { onDelete: "cascade" }),
|
||||
kind: text("kind").notNull().default("transcode"),
|
||||
status: photoJobStatus("status").notNull().default("pending"),
|
||||
attempts: integer("attempts").notNull().default(0),
|
||||
lastError: text("last_error"),
|
||||
...timestamps,
|
||||
},
|
||||
(table) => [
|
||||
index("photo_jobs_status_idx").on(table.status),
|
||||
index("photo_jobs_photo_id_idx").on(table.photoId),
|
||||
],
|
||||
);
|
||||
|
||||
export const auditEvents = pgTable(
|
||||
"audit_events",
|
||||
{
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
groupId: uuid("group_id").references(() => groups.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
eventId: uuid("event_id").references(() => events.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
actorUserId: text("actor_user_id").references(() => user.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
action: text("action").notNull(),
|
||||
subjectType: text("subject_type").notNull(),
|
||||
subjectId: text("subject_id").notNull(),
|
||||
metadata: jsonb("metadata")
|
||||
.$type<Record<string, string | number | boolean | null>>()
|
||||
.notNull()
|
||||
.default(sql`'{}'::jsonb`),
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index("audit_events_group_id_idx").on(table.groupId),
|
||||
index("audit_events_event_id_idx").on(table.eventId),
|
||||
index("audit_events_created_at_idx").on(table.createdAt),
|
||||
],
|
||||
);
|
||||
|
||||
export const rateLimitBuckets = pgTable("rate_limit_buckets", {
|
||||
key: text("key").primaryKey(),
|
||||
count: integer("count").notNull(),
|
||||
resetAt: timestamp("reset_at", { withTimezone: true }).notNull(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
});
|
||||
|
||||
export const groupRelations = relations(groups, ({ many }) => ({
|
||||
memberships: many(groupMemberships),
|
||||
events: many(events),
|
||||
entitlements: many(entitlements),
|
||||
}));
|
||||
|
||||
export const groupMembershipRelations = relations(
|
||||
groupMemberships,
|
||||
({ one }) => ({
|
||||
group: one(groups, {
|
||||
fields: [groupMemberships.groupId],
|
||||
references: [groups.id],
|
||||
}),
|
||||
user: one(user, {
|
||||
fields: [groupMemberships.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
export const eventRelations = relations(events, ({ one, many }) => ({
|
||||
group: one(groups, {
|
||||
fields: [events.groupId],
|
||||
references: [groups.id],
|
||||
}),
|
||||
memberships: many(eventMemberships),
|
||||
photos: many(photos),
|
||||
guests: many(guests),
|
||||
submissions: many(submissions),
|
||||
}));
|
||||
|
||||
export const eventMembershipRelations = relations(
|
||||
eventMemberships,
|
||||
({ one }) => ({
|
||||
event: one(events, {
|
||||
fields: [eventMemberships.eventId],
|
||||
references: [events.id],
|
||||
}),
|
||||
user: one(user, {
|
||||
fields: [eventMemberships.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
export const guestRelations = relations(guests, ({ one, many }) => ({
|
||||
event: one(events, {
|
||||
fields: [guests.eventId],
|
||||
references: [events.id],
|
||||
}),
|
||||
submissions: many(submissions),
|
||||
}));
|
||||
|
||||
export const submissionRelations = relations(submissions, ({ one, many }) => ({
|
||||
event: one(events, {
|
||||
fields: [submissions.eventId],
|
||||
references: [events.id],
|
||||
}),
|
||||
guest: one(guests, {
|
||||
fields: [submissions.guestId],
|
||||
references: [guests.id],
|
||||
}),
|
||||
photos: many(photos),
|
||||
}));
|
||||
|
||||
export const photoRelations = relations(photos, ({ one, many }) => ({
|
||||
event: one(events, {
|
||||
fields: [photos.eventId],
|
||||
references: [events.id],
|
||||
}),
|
||||
submission: one(submissions, {
|
||||
fields: [photos.submissionId],
|
||||
references: [submissions.id],
|
||||
}),
|
||||
jobs: many(photoJobs),
|
||||
}));
|
||||
|
||||
export const photoJobRelations = relations(photoJobs, ({ one }) => ({
|
||||
photo: one(photos, {
|
||||
fields: [photoJobs.photoId],
|
||||
references: [photos.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const platformAdministratorRelations = relations(
|
||||
platformAdministrators,
|
||||
({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [platformAdministrators.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
export const entitlementRelations = relations(entitlements, ({ one }) => ({
|
||||
group: one(groups, {
|
||||
fields: [entitlements.groupId],
|
||||
references: [groups.id],
|
||||
}),
|
||||
}));
|
||||
@@ -0,0 +1,173 @@
|
||||
import "dotenv/config";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import {
|
||||
closeDb,
|
||||
deploymentSettings,
|
||||
entitlements,
|
||||
eventMemberships,
|
||||
events,
|
||||
getDb,
|
||||
groupMemberships,
|
||||
groups,
|
||||
platformAdministrators,
|
||||
user,
|
||||
} from "./index";
|
||||
|
||||
const db = getDb();
|
||||
|
||||
async function userByEmail(email: string) {
|
||||
const [row] = await db.select().from(user).where(eq(user.email, email)).limit(1);
|
||||
return row ?? null;
|
||||
}
|
||||
|
||||
const admin = await userByEmail("admin@example.com");
|
||||
const host = await userByEmail("host@example.com");
|
||||
const partner = await userByEmail("partner@example.com");
|
||||
const manager = await userByEmail("manager@example.com");
|
||||
|
||||
if (!admin || !host || !partner || !manager) {
|
||||
console.info(
|
||||
"Seed users missing. Run bun run auth:seed, then bun run db:seed again.",
|
||||
);
|
||||
await closeDb();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
await db
|
||||
.insert(deploymentSettings)
|
||||
.values({ id: "default" })
|
||||
.onConflictDoNothing();
|
||||
|
||||
const [existingAdmin] = await db
|
||||
.select()
|
||||
.from(platformAdministrators)
|
||||
.where(eq(platformAdministrators.userId, admin.id))
|
||||
.limit(1);
|
||||
if (!existingAdmin) {
|
||||
await db.insert(platformAdministrators).values({
|
||||
userId: admin.id,
|
||||
role: "super_admin",
|
||||
});
|
||||
}
|
||||
|
||||
let [group] = await db
|
||||
.select()
|
||||
.from(groups)
|
||||
.where(eq(groups.slug, "demo-family"))
|
||||
.limit(1);
|
||||
if (!group) {
|
||||
[group] = await db
|
||||
.insert(groups)
|
||||
.values({
|
||||
name: "Demo family",
|
||||
slug: "demo-family",
|
||||
createdByUserId: host.id,
|
||||
})
|
||||
.returning();
|
||||
}
|
||||
|
||||
if (group) {
|
||||
for (const member of [
|
||||
{ userId: host.id, role: "owner" as const },
|
||||
{ userId: partner.id, role: "owner" as const },
|
||||
{ userId: manager.id, role: "member" as const },
|
||||
]) {
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(groupMemberships)
|
||||
.where(
|
||||
and(
|
||||
eq(groupMemberships.groupId, group.id),
|
||||
eq(groupMemberships.userId, member.userId),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
if (!existing) {
|
||||
await db.insert(groupMemberships).values({
|
||||
groupId: group.id,
|
||||
userId: member.userId,
|
||||
role: member.role,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const [entitlement] = await db
|
||||
.select()
|
||||
.from(entitlements)
|
||||
.where(eq(entitlements.groupId, group.id))
|
||||
.limit(1);
|
||||
if (!entitlement) {
|
||||
await db.insert(entitlements).values({
|
||||
groupId: group.id,
|
||||
eventLimit: null,
|
||||
complimentary: true,
|
||||
source: "platform",
|
||||
grantedByUserId: admin.id,
|
||||
});
|
||||
}
|
||||
|
||||
let [event] = await db
|
||||
.select()
|
||||
.from(events)
|
||||
.where(eq(events.slug, "demo"))
|
||||
.limit(1);
|
||||
if (!event) {
|
||||
[event] = await db
|
||||
.insert(events)
|
||||
.values({
|
||||
groupId: group.id,
|
||||
slug: "demo",
|
||||
title: "Demo gathering",
|
||||
description:
|
||||
"A sample event so you can try guest uploads and host moderation.",
|
||||
status: "published",
|
||||
listed: true,
|
||||
uploadEnabled: true,
|
||||
galleryReleasedAt: new Date(),
|
||||
startsAt: new Date(),
|
||||
})
|
||||
.returning();
|
||||
console.info("Seeded published event /e/demo");
|
||||
} else {
|
||||
await db
|
||||
.update(events)
|
||||
.set({
|
||||
groupId: group.id,
|
||||
listed: true,
|
||||
galleryReleasedAt: event.galleryReleasedAt ?? new Date(),
|
||||
status: "published",
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(events.id, event.id));
|
||||
console.info("Demo event already exists");
|
||||
}
|
||||
|
||||
if (event) {
|
||||
for (const member of [
|
||||
{ userId: host.id, role: "owner" as const },
|
||||
{ userId: partner.id, role: "owner" as const },
|
||||
{ userId: manager.id, role: "manager" as const },
|
||||
]) {
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(eventMemberships)
|
||||
.where(
|
||||
and(
|
||||
eq(eventMemberships.eventId, event.id),
|
||||
eq(eventMemberships.userId, member.userId),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
if (!existing) {
|
||||
await db.insert(eventMemberships).values({
|
||||
eventId: event.id,
|
||||
userId: member.userId,
|
||||
role: member.role,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.info("Seeded demo group, two owners, a manager, and platform super-admin.");
|
||||
await closeDb();
|
||||
Reference in New Issue
Block a user