Refine workspaces and event publishing; harden uploads and email delivery
This commit is contained in:
@@ -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>;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user