Refine workspaces and event publishing; harden uploads and email delivery

This commit is contained in:
2026-09-09 15:44:00 -04:00
parent f5702caaea
commit 574f29a68e
93 changed files with 2885 additions and 535 deletions
-13
View File
@@ -1,21 +1,8 @@
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>;
+59
View File
@@ -1,5 +1,7 @@
import {
boolean,
check,
doublePrecision,
index,
integer,
jsonb,
@@ -138,21 +140,76 @@ export const events = pgTable(
slug: text("slug").notNull(),
title: text("title").notNull(),
description: text("description"),
location: text("location"),
latitude: doublePrecision("latitude"),
longitude: doublePrecision("longitude"),
bannerPhotoId: uuid("banner_photo_id"),
customBannerId: uuid("custom_banner_id"),
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 }),
publishAt: timestamp("publish_at", { withTimezone: true }),
submissionsOpenAt: timestamp("submissions_open_at", { withTimezone: true }),
submissionsCloseAt: timestamp("submissions_close_at", { withTimezone: true }),
galleryVisibleAt: timestamp("gallery_visible_at", { withTimezone: true }),
notesVisibleAt: timestamp("notes_visible_at", { withTimezone: true }),
completedAt: timestamp("completed_at", { withTimezone: true }),
notesPolicy: text("notes_policy").$type<"automatic" | "approved" | "never">().notNull().default("never"),
galleryPolicy: text("gallery_policy").$type<"automatic" | "approved" | "never">().notNull().default("approved"),
showPhotoStats: boolean("show_photo_stats").notNull().default(false),
showSubmitterStats: boolean("show_submitter_stats").notNull().default(false),
showNoteStats: boolean("show_note_stats").notNull().default(false),
...timestamps,
},
(table) => [
uniqueIndex("events_slug_idx").on(table.slug),
check("events_notes_policy_check", sql`${table.notesPolicy} IN ('automatic', 'approved', 'never')`),
check("events_gallery_policy_check", sql`${table.galleryPolicy} IN ('automatic', 'approved', 'never')`),
check("events_coordinates_check", sql`(${table.latitude} IS NULL AND ${table.longitude} IS NULL) OR (${table.latitude} IS NOT NULL AND ${table.longitude} IS NOT NULL AND ${table.latitude} BETWEEN -90 AND 90 AND ${table.longitude} BETWEEN -180 AND 180)`),
index("events_group_id_idx").on(table.groupId),
index("events_listed_idx").on(table.listed, table.status),
],
);
export const emailDeliveries = pgTable("email_deliveries", {
id: uuid("id").defaultRandom().primaryKey(),
eventId: uuid("event_id").notNull().references(() => events.id, { onDelete: "cascade" }),
recipient: text("recipient").notNull(),
payload: jsonb("payload").$type<import("@album/contracts").EmailPayload>().notNull(),
provider: text("provider").notNull(),
status: text("status").notNull().default("pending"),
attempts: integer("attempts").notNull().default(0),
firstAttemptAt: timestamp("first_attempt_at", { withTimezone: true }),
nextAttemptAt: timestamp("next_attempt_at", { withTimezone: true }).notNull().defaultNow(),
providerId: text("provider_id"),
lastError: text("last_error"),
...timestamps,
}, (table) => [uniqueIndex("email_deliveries_event_recipient_idx").on(table.eventId, table.recipient), index("email_deliveries_queue_idx").on(table.status, table.nextAttemptAt)]);
// Provider callbacks may precede the worker's providerId write. Keep a minimal
// inbox independently, without storing recipient addresses or raw payloads.
export const emailWebhookEvents = pgTable("email_webhook_events", {
id: text("id").primaryKey(),
providerId: text("provider_id").notNull(),
outcome: text("outcome").notNull(),
occurredAt: timestamp("occurred_at", { withTimezone: true }).notNull(),
receivedAt: timestamp("received_at", { withTimezone: true }).notNull().defaultNow(),
}, (table) => [index("email_webhook_events_provider_idx").on(table.providerId)]);
export const eventBanners = pgTable("event_banners", {
id: uuid("id").defaultRandom().primaryKey(),
eventId: uuid("event_id").notNull().references(() => events.id, { onDelete: "cascade" }),
originalKey: text("original_key").notNull(),
displayKey: text("display_key"),
contentType: text("content_type").notNull(),
byteSize: integer("byte_size").notNull(),
status: text("status").notNull().default("uploading"),
...timestamps,
}, (table) => [index("event_banners_event_idx").on(table.eventId), index("event_banners_status_idx").on(table.status)]);
export const eventMemberships = pgTable(
"event_memberships",
{
@@ -272,7 +329,9 @@ export const guests = pgTable(
email: text("email"),
notifyWhenReady: boolean("notify_when_ready").notNull().default(false),
notifiedAt: timestamp("notified_at", { withTimezone: true }),
notificationClaimedAt: timestamp("notification_claimed_at", { withTimezone: true }),
note: text("note"),
noteApproved: boolean("note_approved").notNull().default(false),
tokenHash: text("token_hash").notNull(),
...timestamps,
},
+10 -3
View File
@@ -117,14 +117,15 @@ if (group) {
.values({
groupId: group.id,
slug: "demo",
title: "Demo gathering",
title: "Riverhead Raceway Championship Night",
description:
"A sample event so you can try guest uploads and host moderation.",
"A late-summer night under the lights at Riverhead Raceway — feature winners, victory lane, and the people who make race night happen.",
status: "published",
listed: true,
uploadEnabled: true,
galleryReleasedAt: new Date(),
startsAt: new Date(),
startsAt: new Date("2026-09-06T22:00:00.000Z"),
endsAt: new Date("2026-09-07T02:00:00.000Z"),
})
.returning();
console.info("Seeded published event /e/demo");
@@ -133,8 +134,14 @@ if (group) {
.update(events)
.set({
groupId: group.id,
title: "Riverhead Raceway Championship Night",
description:
"A late-summer night under the lights at Riverhead Raceway — feature winners, victory lane, and the people who make race night happen.",
listed: true,
uploadEnabled: true,
galleryReleasedAt: event.galleryReleasedAt ?? new Date(),
startsAt: new Date("2026-09-06T22:00:00.000Z"),
endsAt: new Date("2026-09-07T02:00:00.000Z"),
status: "published",
updatedAt: new Date(),
})
@@ -0,0 +1,21 @@
import postgres from "postgres";
import { drizzle } from "drizzle-orm/postgres-js";
import { migrate } from "drizzle-orm/postgres-js/migrator";
const url = new URL(process.env.DATABASE_URL ?? "postgres://album:album@localhost:5439/album");
if (!["localhost", "127.0.0.1"].includes(url.hostname)) throw new Error("Clean migration verification is local-only");
const admin = postgres(url.toString(), { max: 1 });
const databaseName = `manyangles_verify_${crypto.randomUUID().replaceAll("-", "")}`;
await admin`CREATE DATABASE ${admin(databaseName)}`;
url.pathname = `/${databaseName}`;
const client = postgres(url.toString(), { max: 1 });
try {
await migrate(drizzle(client), { migrationsFolder: new URL("../drizzle", import.meta.url).pathname });
const rows = await client`SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name IN ('email_deliveries', 'events', 'guests', 'event_banners')`;
if (rows.length !== 4) throw new Error("Expected lifecycle/outbox tables were not created");
console.info("Clean-database migrations passed");
} finally {
await client.end();
await admin`DROP DATABASE ${admin(databaseName)}`;
await admin.end();
}