From 736d738ba015af9def87838b419e8576a5c2ad04 Mon Sep 17 00:00:00 2001 From: Niko Yu <137782638+thesleepyniko@users.noreply.github.com> Date: Mon, 20 Apr 2026 21:12:07 +0000 Subject: [PATCH 01/20] feat: add exceptions to schema --- .../src/lib/server/db/schema.ts | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/resolution-frontend/src/lib/server/db/schema.ts b/resolution-frontend/src/lib/server/db/schema.ts index d31c200..a4eb93d 100644 --- a/resolution-frontend/src/lib/server/db/schema.ts +++ b/resolution-frontend/src/lib/server/db/schema.ts @@ -139,6 +139,34 @@ export const userPathway = pgTable('user_pathway', { uniqueIndex('user_pathway_unique_idx').on(table.userId, table.pathway) ]); +export const submissionClosureException = pgTable('submission_closure_exception', { + id: text('id').primaryKey().$defaultFn(() => createId()), + userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }), + seasonId: text('season_id').notNull().references(() => programSeason.id, { onDelete: 'cascade' }), + pathway: pathwayEnum('pathway').notNull(), + weekNumber: integer('week_number').notNull(), + reason: text('reason').notNull(), + expiresAt: timestamp('expires_at', { mode: 'date' }).notNull(), + createdAt: timestamp('created_at', { mode: 'date' }).notNull().defaultNow(), + createdBy: text('created_by').notNull().references(() => user.id, { onDelete: 'cascade' }) +}, (table) => [ + uniqueIndex('submission_exception_unique_idx').on( + table.userId, + table.seasonId, + table.pathway, + table.weekNumber + ), + index('submission_exception_lookup_idx').on( + table.seasonId, + table.pathway, + table.weekNumber + ), + index('submission_exception_user_idx').on( + table.userId, + table.seasonId + ) +]); + // Relations export const userRelations = relations(user, ({ many }) => ({ pathways: many(userPathway), @@ -149,7 +177,8 @@ export const userRelations = relations(user, ({ many }) => ({ weeklyShips: many(weeklyShip), payouts: many(ambassadorPayout), referralLinks: many(referralLink), - reviewerAssignments: many(reviewerPathway) + reviewerAssignments: many(reviewerPathway), + exceptions: many(submissionClosureException) })); export const sessionRelations = relations(session, ({ one }) => ({ @@ -161,7 +190,8 @@ export const programSeasonRelations = relations(programSeason, ({ many }) => ({ workshops: many(workshop), completions: many(workshopCompletion), weeklyShips: many(weeklyShip), - payouts: many(ambassadorPayout) + payouts: many(ambassadorPayout), + exceptions: many(submissionClosureException) })); export const programEnrollmentRelations = relations(programEnrollment, ({ one }) => ({ @@ -209,6 +239,12 @@ export const userPathwayRelations = relations(userPathway, ({ one }) => ({ user: one(user, { fields: [userPathway.userId], references: [user.id] }) })); +export const submissionClosureExceptionRelations = relations(submissionClosureException, ({ one }) => ({ + user: one(user, { fields: [submissionClosureException.userId], references: [user.id] }), + createdBy: one(user, { fields: [submissionClosureException.createdBy], references: [user.id] }), + season: one(programSeason, { fields: [submissionClosureException.seasonId], references: [programSeason.id]}) +})) + // Ambassador pathway assignments - which pathways an ambassador can edit export const ambassadorPathway = pgTable('ambassador_pathway', { id: text('id').primaryKey().$defaultFn(() => createId()), From 8eacfff3d1bc38bd439bb25b682dcba03a6a43ca Mon Sep 17 00:00:00 2001 From: Niko Yu <137782638+thesleepyniko@users.noreply.github.com> Date: Mon, 20 Apr 2026 21:13:35 +0000 Subject: [PATCH 02/20] feat: add isActive for manual expiry --- resolution-frontend/src/lib/server/db/schema.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/resolution-frontend/src/lib/server/db/schema.ts b/resolution-frontend/src/lib/server/db/schema.ts index a4eb93d..2161ff4 100644 --- a/resolution-frontend/src/lib/server/db/schema.ts +++ b/resolution-frontend/src/lib/server/db/schema.ts @@ -146,6 +146,7 @@ export const submissionClosureException = pgTable('submission_closure_exception' pathway: pathwayEnum('pathway').notNull(), weekNumber: integer('week_number').notNull(), reason: text('reason').notNull(), + isActive: boolean('is_active').notNull().default(true), expiresAt: timestamp('expires_at', { mode: 'date' }).notNull(), createdAt: timestamp('created_at', { mode: 'date' }).notNull().defaultNow(), createdBy: text('created_by').notNull().references(() => user.id, { onDelete: 'cascade' }) From 2a7960dff702396385b30ef7187cbd200e165581 Mon Sep 17 00:00:00 2001 From: Niko Yu <137782638+thesleepyniko@users.noreply.github.com> Date: Tue, 21 Apr 2026 02:38:03 +0000 Subject: [PATCH 03/20] feat: add template for exceptions page --- .../app/ambassador/exceptions/+page.server.ts | 163 +++++ .../app/ambassador/exceptions/+page.svelte | 664 ++++++++++++++++++ 2 files changed, 827 insertions(+) create mode 100644 resolution-frontend/src/routes/app/ambassador/exceptions/+page.server.ts create mode 100644 resolution-frontend/src/routes/app/ambassador/exceptions/+page.svelte diff --git a/resolution-frontend/src/routes/app/ambassador/exceptions/+page.server.ts b/resolution-frontend/src/routes/app/ambassador/exceptions/+page.server.ts new file mode 100644 index 0000000..9229d01 --- /dev/null +++ b/resolution-frontend/src/routes/app/ambassador/exceptions/+page.server.ts @@ -0,0 +1,163 @@ +import type { PageServerLoad, Actions } from './$types'; +import { db } from '$lib/server/db'; +import { ambassadorPathway, referralLink, referralSignup, user } from '$lib/server/db/schema'; +import { eq, and } from 'drizzle-orm'; +import { error, fail } from '@sveltejs/kit'; +import { createId } from '@paralleldrive/cuid2'; +import { PATHWAY_IDS } from '$lib/pathways'; + +function generateReferralCode(): string { + return createId().slice(0, 8); +} + +export const load: PageServerLoad = async ({ parent }) => { + const { user: currentUser } = await parent(); + + const assignments = await db + .select() + .from(ambassadorPathway) + .where(eq(ambassadorPathway.userId, currentUser.id)); + + if (assignments.length === 0 && !currentUser.isAdmin) { + throw error(403, 'You are not an ambassador'); + } + + const links = await db + .select() + .from(referralLink) + .where(eq(referralLink.ambassadorId, currentUser.id)); + + const linksWithSignups = await Promise.all( + links.map(async (link) => { + const signups = await db + .select({ + id: referralSignup.id, + createdAt: referralSignup.createdAt, + userId: user.id, + firstName: user.firstName, + lastName: user.lastName, + email: user.email + }) + .from(referralSignup) + .innerJoin(user, eq(referralSignup.userId, user.id)) + .where(eq(referralSignup.referralLinkId, link.id)); + + return { + ...link, + signups + }; + }) + ); + + return { + assignments: assignments.map((a) => a.pathway), + referralLinks: linksWithSignups + }; +}; + +export const actions: Actions = { + createLink: async ({ request, locals }) => { + if (!locals.user || !locals.session) { + return fail(401, { error: 'Unauthorized' }); + } + + const formData = await request.formData(); + const pathway = formData.get('pathway') as string; + const label = formData.get('label') as string | null; + const customSlug = (formData.get('slug') as string | null)?.trim() || null; + + const validPathways = PATHWAY_IDS; + if (!pathway || !validPathways.includes(pathway)) { + return fail(400, { error: 'Invalid pathway' }); + } + + let code: string; + if (customSlug) { + if (!/^[a-zA-Z0-9_-]{3,32}$/.test(customSlug)) { + return fail(400, { error: 'Slug must be 3-32 characters, letters, numbers, hyphens, or underscores only' }); + } + const existing = await db.query.referralLink.findFirst({ + where: eq(referralLink.code, customSlug) + }); + if (existing) { + return fail(400, { error: 'That slug is already taken' }); + } + code = customSlug; + } else { + code = generateReferralCode(); + } + + const assignment = await db.query.ambassadorPathway.findFirst({ + where: and( + eq(ambassadorPathway.userId, locals.user.id), + eq(ambassadorPathway.pathway, pathway as any) + ) + }); + + if (!assignment && !locals.user.isAdmin) { + return fail(403, { error: 'You are not assigned to this pathway' }); + } + + await db.insert(referralLink).values({ + ambassadorId: locals.user.id, + pathway: pathway as any, + code, + label: label || null + }); + + return { success: true }; + }, + + toggleLink: async ({ request, locals }) => { + if (!locals.user || !locals.session) { + return fail(401, { error: 'Unauthorized' }); + } + + const formData = await request.formData(); + const linkId = formData.get('linkId') as string; + + if (!linkId) { + return fail(400, { error: 'Missing link ID' }); + } + + const link = await db.query.referralLink.findFirst({ + where: and(eq(referralLink.id, linkId), eq(referralLink.ambassadorId, locals.user.id)) + }); + + if (!link) { + return fail(404, { error: 'Link not found' }); + } + + await db + .update(referralLink) + .set({ isActive: !link.isActive }) + .where(eq(referralLink.id, linkId)); + + return { success: true }; + }, + + deleteLink: async ({ request, locals }) => { + if (!locals.user || !locals.session) { + return fail(401, { error: 'Unauthorized' }); + } + + const formData = await request.formData(); + const linkId = formData.get('linkId') as string; + + if (!linkId) { + return fail(400, { error: 'Missing link ID' }); + } + + const link = await db.query.referralLink.findFirst({ + where: and(eq(referralLink.id, linkId), eq(referralLink.ambassadorId, locals.user.id)) + }); + + if (!link) { + return fail(404, { error: 'Link not found' }); + } + + await db.delete(referralLink).where(eq(referralLink.id, linkId)); + + return { success: true }; + } +}; diff --git a/resolution-frontend/src/routes/app/ambassador/exceptions/+page.svelte b/resolution-frontend/src/routes/app/ambassador/exceptions/+page.svelte new file mode 100644 index 0000000..b9a248d --- /dev/null +++ b/resolution-frontend/src/routes/app/ambassador/exceptions/+page.svelte @@ -0,0 +1,664 @@ + + + + Referral Links - Resolution + + + +
+ + Back + Back to Ambassador Dashboard + + +
+

Referral Links

+

Create and manage referral links for your pathways

+
+ + +
+

Create Referral Link

+
+
+
+ + +
+
+ + + /ref/{linkSlug || 'auto-generated'} +
+
+ + +
+ +
+
+
+ + + {#if data.referralLinks.length === 0} +
+

You haven't created any referral links yet.

+

Use the form above to create your first link.

+
+ {:else} + {#each pathwaysWithLinks as pathway} + {@const info = pathwayInfo[pathway]} + {@const links = getLinksByPathway(pathway)} +
+
+ {info.label} +

{info.label}

+ {links.length} link{links.length !== 1 ? 's' : ''} +
+ + +
+ {/each} + {/if} +
+
+ + From 741be26b08981f12b084c46a72f1d6983ac3c781 Mon Sep 17 00:00:00 2001 From: Niko Yu <137782638+thesleepyniko@users.noreply.github.com> Date: Mon, 20 Apr 2026 23:07:07 -0400 Subject: [PATCH 04/20] feat: db changes & migrations --- .../drizzle/0005_productive_madripoor.sql | 19 + .../drizzle/meta/0005_snapshot.json | 1816 +++++++++++++++++ .../drizzle/meta/_journal.json | 7 + 3 files changed, 1842 insertions(+) create mode 100644 resolution-frontend/drizzle/0005_productive_madripoor.sql create mode 100644 resolution-frontend/drizzle/meta/0005_snapshot.json diff --git a/resolution-frontend/drizzle/0005_productive_madripoor.sql b/resolution-frontend/drizzle/0005_productive_madripoor.sql new file mode 100644 index 0000000..b7c5ff3 --- /dev/null +++ b/resolution-frontend/drizzle/0005_productive_madripoor.sql @@ -0,0 +1,19 @@ +CREATE TABLE "submission_closure_exception" ( + "id" text PRIMARY KEY NOT NULL, + "user_id" text NOT NULL, + "season_id" text NOT NULL, + "pathway" "pathway" NOT NULL, + "week_number" integer NOT NULL, + "reason" text NOT NULL, + "is_active" boolean DEFAULT true NOT NULL, + "expires_at" timestamp NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "created_by" text NOT NULL +); +--> statement-breakpoint +ALTER TABLE "submission_closure_exception" ADD CONSTRAINT "submission_closure_exception_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "submission_closure_exception" ADD CONSTRAINT "submission_closure_exception_season_id_program_season_id_fk" FOREIGN KEY ("season_id") REFERENCES "public"."program_season"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "submission_closure_exception" ADD CONSTRAINT "submission_closure_exception_created_by_user_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "submission_exception_unique_idx" ON "submission_closure_exception" USING btree ("user_id","season_id","pathway","week_number");--> statement-breakpoint +CREATE INDEX "submission_exception_lookup_idx" ON "submission_closure_exception" USING btree ("season_id","pathway","week_number");--> statement-breakpoint +CREATE INDEX "submission_exception_user_idx" ON "submission_closure_exception" USING btree ("user_id","season_id"); \ No newline at end of file diff --git a/resolution-frontend/drizzle/meta/0005_snapshot.json b/resolution-frontend/drizzle/meta/0005_snapshot.json new file mode 100644 index 0000000..53df726 --- /dev/null +++ b/resolution-frontend/drizzle/meta/0005_snapshot.json @@ -0,0 +1,1816 @@ +{ + "id": "097f7629-1e63-459c-803a-0dcbfbca8123", + "prevId": "a3a209aa-49a0-47e6-9126-892277549771", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.ambassador_pathway": { + "name": "ambassador_pathway", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pathway": { + "name": "pathway", + "type": "pathway", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "ambassador_pathway_unique_idx": { + "name": "ambassador_pathway_unique_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pathway", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ambassador_pathway_user_id_user_id_fk": { + "name": "ambassador_pathway_user_id_user_id_fk", + "tableFrom": "ambassador_pathway", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ambassador_pathway_assigned_by_user_id_fk": { + "name": "ambassador_pathway_assigned_by_user_id_fk", + "tableFrom": "ambassador_pathway", + "tableTo": "user", + "columnsFrom": [ + "assigned_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ambassador_payout": { + "name": "ambassador_payout", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ambassador_id": { + "name": "ambassador_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "season_id": { + "name": "season_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_cents": { + "name": "amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "payout_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'DRAFT'" + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "paid_at": { + "name": "paid_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "ambassador_payout_ambassador_id_user_id_fk": { + "name": "ambassador_payout_ambassador_id_user_id_fk", + "tableFrom": "ambassador_payout", + "tableTo": "user", + "columnsFrom": [ + "ambassador_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ambassador_payout_season_id_program_season_id_fk": { + "name": "ambassador_payout_season_id_program_season_id_fk", + "tableFrom": "ambassador_payout", + "tableTo": "program_season", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ambassador_payout_item": { + "name": "ambassador_payout_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "payout_id": { + "name": "payout_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workshop_id": { + "name": "workshop_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "completion_count": { + "name": "completion_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "rate_cents_per_completion": { + "name": "rate_cents_per_completion", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "amount_cents": { + "name": "amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "ambassador_payout_item_payout_id_ambassador_payout_id_fk": { + "name": "ambassador_payout_item_payout_id_ambassador_payout_id_fk", + "tableFrom": "ambassador_payout_item", + "tableTo": "ambassador_payout", + "columnsFrom": [ + "payout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ambassador_payout_item_workshop_id_workshop_id_fk": { + "name": "ambassador_payout_item_workshop_id_workshop_id_fk", + "tableFrom": "ambassador_payout_item", + "tableTo": "workshop", + "columnsFrom": [ + "workshop_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pathway_week_content": { + "name": "pathway_week_content", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "pathway": { + "name": "pathway", + "type": "pathway", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "week_number": { + "name": "week_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "prize_image_url": { + "name": "prize_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_submissions_open": { + "name": "is_submissions_open", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_edited_by": { + "name": "last_edited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pathway_week_content_unique_idx": { + "name": "pathway_week_content_unique_idx", + "columns": [ + { + "expression": "pathway", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "week_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pathway_week_content_last_edited_by_user_id_fk": { + "name": "pathway_week_content_last_edited_by_user_id_fk", + "tableFrom": "pathway_week_content", + "tableTo": "user", + "columnsFrom": [ + "last_edited_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.program_enrollment": { + "name": "program_enrollment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "season_id": { + "name": "season_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "enrollment_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "enrollment_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ACTIVE'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "starting_week": { + "name": "starting_week", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "enrollment_user_season_role_idx": { + "name": "enrollment_user_season_role_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "program_enrollment_user_id_user_id_fk": { + "name": "program_enrollment_user_id_user_id_fk", + "tableFrom": "program_enrollment", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "program_enrollment_season_id_program_season_id_fk": { + "name": "program_enrollment_season_id_program_season_id_fk", + "tableFrom": "program_enrollment", + "tableTo": "program_season", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.program_season": { + "name": "program_season", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signup_opens_at": { + "name": "signup_opens_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "signup_closes_at": { + "name": "signup_closes_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "starts_at": { + "name": "starts_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ends_at": { + "name": "ends_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "total_weeks": { + "name": "total_weeks", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 8 + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "program_season_slug_unique": { + "name": "program_season_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.referral_link": { + "name": "referral_link", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ambassador_id": { + "name": "ambassador_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pathway": { + "name": "pathway", + "type": "pathway", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "referral_link_ambassador_id_user_id_fk": { + "name": "referral_link_ambassador_id_user_id_fk", + "tableFrom": "referral_link", + "tableTo": "user", + "columnsFrom": [ + "ambassador_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "referral_link_code_unique": { + "name": "referral_link_code_unique", + "nullsNotDistinct": false, + "columns": [ + "code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.referral_signup": { + "name": "referral_signup", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "referral_link_id": { + "name": "referral_link_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "referral_signup_unique_idx": { + "name": "referral_signup_unique_idx", + "columns": [ + { + "expression": "referral_link_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "referral_signup_referral_link_id_referral_link_id_fk": { + "name": "referral_signup_referral_link_id_referral_link_id_fk", + "tableFrom": "referral_signup", + "tableTo": "referral_link", + "columnsFrom": [ + "referral_link_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "referral_signup_user_id_user_id_fk": { + "name": "referral_signup_user_id_user_id_fk", + "tableFrom": "referral_signup", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reviewer_pathway": { + "name": "reviewer_pathway", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pathway": { + "name": "pathway", + "type": "pathway", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "reviewer_pathway_unique_idx": { + "name": "reviewer_pathway_unique_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pathway", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reviewer_pathway_user_id_user_id_fk": { + "name": "reviewer_pathway_user_id_user_id_fk", + "tableFrom": "reviewer_pathway", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "reviewer_pathway_assigned_by_user_id_fk": { + "name": "reviewer_pathway_assigned_by_user_id_fk", + "tableFrom": "reviewer_pathway", + "tableTo": "user", + "columnsFrom": [ + "assigned_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.submission_closure_exception": { + "name": "submission_closure_exception", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "season_id": { + "name": "season_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pathway": { + "name": "pathway", + "type": "pathway", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "week_number": { + "name": "week_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "submission_exception_unique_idx": { + "name": "submission_exception_unique_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pathway", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "week_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "submission_exception_lookup_idx": { + "name": "submission_exception_lookup_idx", + "columns": [ + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pathway", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "week_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "submission_exception_user_idx": { + "name": "submission_exception_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "submission_closure_exception_user_id_user_id_fk": { + "name": "submission_closure_exception_user_id_user_id_fk", + "tableFrom": "submission_closure_exception", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "submission_closure_exception_season_id_program_season_id_fk": { + "name": "submission_closure_exception_season_id_program_season_id_fk", + "tableFrom": "submission_closure_exception", + "tableTo": "program_season", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "submission_closure_exception_created_by_user_id_fk": { + "name": "submission_closure_exception_created_by_user_id_fk", + "tableFrom": "submission_closure_exception", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hack_club_id": { + "name": "hack_club_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slack_id": { + "name": "slack_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verification_status": { + "name": "verification_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ysws_eligible": { + "name": "ysws_eligible", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "user_hack_club_id_unique": { + "name": "user_hack_club_id_unique", + "nullsNotDistinct": false, + "columns": [ + "hack_club_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_pathway": { + "name": "user_pathway", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pathway": { + "name": "pathway", + "type": "pathway", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_pathway_unique_idx": { + "name": "user_pathway_unique_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pathway", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_pathway_user_id_user_id_fk": { + "name": "user_pathway_user_id_user_id_fk", + "tableFrom": "user_pathway", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.weekly_ship": { + "name": "weekly_ship", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "season_id": { + "name": "season_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workshop_id": { + "name": "workshop_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "week_number": { + "name": "week_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "goal_text": { + "name": "goal_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "ship_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'PLANNED'" + }, + "proof_url": { + "name": "proof_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shipped_at": { + "name": "shipped_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ship_user_season_week_idx": { + "name": "ship_user_season_week_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "week_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "weekly_ship_user_id_user_id_fk": { + "name": "weekly_ship_user_id_user_id_fk", + "tableFrom": "weekly_ship", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "weekly_ship_season_id_program_season_id_fk": { + "name": "weekly_ship_season_id_program_season_id_fk", + "tableFrom": "weekly_ship", + "tableTo": "program_season", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "weekly_ship_workshop_id_workshop_id_fk": { + "name": "weekly_ship_workshop_id_workshop_id_fk", + "tableFrom": "weekly_ship", + "tableTo": "workshop", + "columnsFrom": [ + "workshop_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workshop": { + "name": "workshop", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "season_id": { + "name": "season_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pathway": { + "name": "pathway", + "type": "pathway", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "difficulty": { + "name": "difficulty", + "type": "difficulty", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "estimated_hours": { + "name": "estimated_hours", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "published": { + "name": "published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workshop_author_id_user_id_fk": { + "name": "workshop_author_id_user_id_fk", + "tableFrom": "workshop", + "tableTo": "user", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workshop_season_id_program_season_id_fk": { + "name": "workshop_season_id_program_season_id_fk", + "tableFrom": "workshop", + "tableTo": "program_season", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workshop_analytics": { + "name": "workshop_analytics", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workshop_id": { + "name": "workshop_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "starts": { + "name": "starts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "completions": { + "name": "completions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "avg_completion_mins": { + "name": "avg_completion_mins", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workshop_analytics_workshop_id_workshop_id_fk": { + "name": "workshop_analytics_workshop_id_workshop_id_fk", + "tableFrom": "workshop_analytics", + "tableTo": "workshop", + "columnsFrom": [ + "workshop_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workshop_analytics_workshop_id_unique": { + "name": "workshop_analytics_workshop_id_unique", + "nullsNotDistinct": false, + "columns": [ + "workshop_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workshop_completion": { + "name": "workshop_completion", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workshop_id": { + "name": "workshop_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "participant_id": { + "name": "participant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "season_id": { + "name": "season_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_url": { + "name": "project_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "completion_workshop_participant_season_idx": { + "name": "completion_workshop_participant_season_idx", + "columns": [ + { + "expression": "workshop_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workshop_completion_workshop_id_workshop_id_fk": { + "name": "workshop_completion_workshop_id_workshop_id_fk", + "tableFrom": "workshop_completion", + "tableTo": "workshop", + "columnsFrom": [ + "workshop_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workshop_completion_participant_id_user_id_fk": { + "name": "workshop_completion_participant_id_user_id_fk", + "tableFrom": "workshop_completion", + "tableTo": "user", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workshop_completion_season_id_program_season_id_fk": { + "name": "workshop_completion_season_id_program_season_id_fk", + "tableFrom": "workshop_completion", + "tableTo": "program_season", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.difficulty": { + "name": "difficulty", + "schema": "public", + "values": [ + "BEGINNER", + "INTERMEDIATE", + "ADVANCED" + ] + }, + "public.enrollment_role": { + "name": "enrollment_role", + "schema": "public", + "values": [ + "PARTICIPANT", + "AMBASSADOR" + ] + }, + "public.enrollment_status": { + "name": "enrollment_status", + "schema": "public", + "values": [ + "ACTIVE", + "DROPPED", + "COMPLETED" + ] + }, + "public.pathway": { + "name": "pathway", + "schema": "public", + "values": [ + "PYTHON", + "RUST", + "GAME_DEV", + "HARDWARE", + "DESIGN", + "GENERAL_CODING" + ] + }, + "public.payout_status": { + "name": "payout_status", + "schema": "public", + "values": [ + "DRAFT", + "PENDING", + "PAID", + "CANCELED" + ] + }, + "public.ship_status": { + "name": "ship_status", + "schema": "public", + "values": [ + "PLANNED", + "IN_PROGRESS", + "SHIPPED", + "MISSED" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/resolution-frontend/drizzle/meta/_journal.json b/resolution-frontend/drizzle/meta/_journal.json index 004721c..3412586 100644 --- a/resolution-frontend/drizzle/meta/_journal.json +++ b/resolution-frontend/drizzle/meta/_journal.json @@ -36,6 +36,13 @@ "when": 1776130671597, "tag": "0004_left_pretty_boy", "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1776740262163, + "tag": "0005_productive_madripoor", + "breakpoints": true } ] } \ No newline at end of file From 43011a68b63669777cdb652dc0c18751e009d86d Mon Sep 17 00:00:00 2001 From: Niko Yu <137782638+thesleepyniko@users.noreply.github.com> Date: Mon, 20 Apr 2026 23:07:27 -0400 Subject: [PATCH 05/20] feat: add exception service --- .../lib/server/services/exceptionService.ts | 42 +++++++++++++++++++ .../src/lib/server/services/index.ts | 1 + 2 files changed, 43 insertions(+) create mode 100644 resolution-frontend/src/lib/server/services/exceptionService.ts diff --git a/resolution-frontend/src/lib/server/services/exceptionService.ts b/resolution-frontend/src/lib/server/services/exceptionService.ts new file mode 100644 index 0000000..e2a884e --- /dev/null +++ b/resolution-frontend/src/lib/server/services/exceptionService.ts @@ -0,0 +1,42 @@ +import { db } from '../db'; +import { submissionClosureException, programSeason } from '../db/schema'; +import { eq, and, gt } from 'drizzle-orm'; +import type { PathwayId } from '$lib/pathways'; + +export const ExceptionService = { + /** + * Check if a user has an active, non-expired exception for a given pathway and week. + * Returns the exception record if found, null otherwise. + */ + async getActiveException( + userId: string, + pathway: PathwayId, + weekNumber: number + ) { + const season = await db.query.programSeason.findFirst({ + where: eq(programSeason.isActive, true) + }); + + if (!season) return null; + + const [exception] = await db + .select({ + id: submissionClosureException.id, + expiresAt: submissionClosureException.expiresAt + }) + .from(submissionClosureException) + .where( + and( + eq(submissionClosureException.userId, userId), + eq(submissionClosureException.seasonId, season.id), + eq(submissionClosureException.pathway, pathway), + eq(submissionClosureException.weekNumber, weekNumber), + eq(submissionClosureException.isActive, true), + gt(submissionClosureException.expiresAt, new Date()) + ) + ) + .limit(1); + + return exception || null; + } +}; diff --git a/resolution-frontend/src/lib/server/services/index.ts b/resolution-frontend/src/lib/server/services/index.ts index 021f500..ba62b66 100644 --- a/resolution-frontend/src/lib/server/services/index.ts +++ b/resolution-frontend/src/lib/server/services/index.ts @@ -1,2 +1,3 @@ export { EnrollmentService } from './enrollmentService'; +export { ExceptionService } from './exceptionService'; export { WeeklyShipService } from './weeklyShipService'; From e34b04f8e9ddec3224620b87350131af5360e412 Mon Sep 17 00:00:00 2001 From: Niko Yu <137782638+thesleepyniko@users.noreply.github.com> Date: Mon, 20 Apr 2026 23:07:59 -0400 Subject: [PATCH 06/20] feat: implement user exceptions --- .../api/ships/submit-project/+server.ts | 6 +- .../src/routes/app/ambassador/+page.svelte | 38 +- .../app/ambassador/exceptions/+page.server.ts | 188 ++--- .../app/ambassador/exceptions/+page.svelte | 644 +++++++++--------- .../[pathway]/week/[week]/+page.server.ts | 9 +- .../[pathway]/week/[week]/+page.svelte | 19 + .../week/[week]/ship/+page.server.ts | 6 +- 7 files changed, 497 insertions(+), 413 deletions(-) diff --git a/resolution-frontend/src/routes/api/ships/submit-project/+server.ts b/resolution-frontend/src/routes/api/ships/submit-project/+server.ts index bb48691..cc11ee9 100644 --- a/resolution-frontend/src/routes/api/ships/submit-project/+server.ts +++ b/resolution-frontend/src/routes/api/ships/submit-project/+server.ts @@ -9,6 +9,7 @@ import { db } from '$lib/server/db'; import { userPathway, pathwayWeekContent } from '$lib/server/db/schema'; import { eq, and } from 'drizzle-orm'; import { type PathwayId } from '$lib/pathways'; +import { ExceptionService } from '$lib/server/services'; const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB (Airtable upload limit) const ALLOWED_IMAGE_TYPES = ['image/png', 'image/jpeg', 'image/gif', 'image/webp']; @@ -76,7 +77,10 @@ export const POST: RequestHandler = async (event) => { } if (!weekContent.isSubmissionsOpen) { - return json({ error: 'Submissions have been closed for this week' }, { status: 403 }); + const exception = await ExceptionService.getActiveException(user.id, parsed.pathway as PathwayId, parsed.week); + if (!exception) { + return json({ error: 'Submissions have been closed for this week' }, { status: 403 }); + } } const base = new Airtable({ apiKey: env.AIRTABLE_API_TOKEN }).base(env.AIRTABLE_BASE_ID); diff --git a/resolution-frontend/src/routes/app/ambassador/+page.svelte b/resolution-frontend/src/routes/app/ambassador/+page.svelte index cd236b6..b429f26 100644 --- a/resolution-frontend/src/routes/app/ambassador/+page.svelte +++ b/resolution-frontend/src/routes/app/ambassador/+page.svelte @@ -40,10 +40,16 @@

Ambassador Dashboard

Manage your pathway content

- - Referrals - Referral Links - +
+ + Referrals + Referral Links + + + Exceptions + Exceptions + +
@@ -158,6 +164,30 @@ background: rgba(255, 255, 255, 1); } + .header-actions { + display: flex; + gap: 0.5rem; + } + + .exceptions-btn { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + background: rgba(255, 255, 255, 0.8); + border: 1px solid #ff8c37; + color: #ff8c37; + border-radius: 20px; + font-family: 'Kodchasan', sans-serif; + text-decoration: none; + white-space: nowrap; + font-size: 0.9rem; + } + + .exceptions-btn:hover { + background: rgba(255, 255, 255, 1); + } + .empty-state { text-align: center; padding: 3rem; diff --git a/resolution-frontend/src/routes/app/ambassador/exceptions/+page.server.ts b/resolution-frontend/src/routes/app/ambassador/exceptions/+page.server.ts index 9229d01..074d43c 100644 --- a/resolution-frontend/src/routes/app/ambassador/exceptions/+page.server.ts +++ b/resolution-frontend/src/routes/app/ambassador/exceptions/+page.server.ts @@ -1,15 +1,10 @@ import type { PageServerLoad, Actions } from './$types'; import { db } from '$lib/server/db'; -import { ambassadorPathway, referralLink, referralSignup, user } from '$lib/server/db/schema'; -import { eq, and } from 'drizzle-orm'; +import { ambassadorPathway, submissionClosureException, programEnrollment, programSeason, user } from '$lib/server/db/schema'; +import { eq, and, inArray } from 'drizzle-orm'; import { error, fail } from '@sveltejs/kit'; -import { createId } from '@paralleldrive/cuid2'; import { PATHWAY_IDS } from '$lib/pathways'; -function generateReferralCode(): string { - return createId().slice(0, 8); -} - export const load: PageServerLoad = async ({ parent }) => { const { user: currentUser } = await parent(); @@ -22,69 +17,95 @@ export const load: PageServerLoad = async ({ parent }) => { throw error(403, 'You are not an ambassador'); } - const links = await db - .select() - .from(referralLink) - .where(eq(referralLink.ambassadorId, currentUser.id)); - - const linksWithSignups = await Promise.all( - links.map(async (link) => { - const signups = await db - .select({ - id: referralSignup.id, - createdAt: referralSignup.createdAt, - userId: user.id, - firstName: user.firstName, - lastName: user.lastName, - email: user.email - }) - .from(referralSignup) - .innerJoin(user, eq(referralSignup.userId, user.id)) - .where(eq(referralSignup.referralLinkId, link.id)); - - return { - ...link, - signups - }; - }) - ); + const assignedPathways = assignments.map((a) => a.pathway); + + const season = await db.query.programSeason.findFirst({ + where: eq(programSeason.isActive, true) + }); + + if (!season) { + throw error(500, 'No active season configured'); + } + + const [enrolledUsers, exceptions] = await Promise.all([ + db + .select({ + id: user.id, + firstName: user.firstName, + lastName: user.lastName, + email: user.email + }) + .from(programEnrollment) + .innerJoin(user, eq(programEnrollment.userId, user.id)) + .where( + and( + eq(programEnrollment.seasonId, season.id), + eq(programEnrollment.status, 'ACTIVE') + ) + ), + + db + .select({ + id: submissionClosureException.id, + userId: submissionClosureException.userId, + pathway: submissionClosureException.pathway, + weekNumber: submissionClosureException.weekNumber, + reason: submissionClosureException.reason, + isActive: submissionClosureException.isActive, + expiresAt: submissionClosureException.expiresAt, + createdAt: submissionClosureException.createdAt, + userName: user.firstName, + userLastName: user.lastName, + userEmail: user.email + }) + .from(submissionClosureException) + .innerJoin(user, eq(submissionClosureException.userId, user.id)) + .where(eq(submissionClosureException.seasonId, season.id)) + ]); return { - assignments: assignments.map((a) => a.pathway), - referralLinks: linksWithSignups + assignments: assignedPathways, + season: { + id: season.id, + name: season.name, + totalWeeks: season.totalWeeks + }, + enrolledUsers, + exceptions }; }; export const actions: Actions = { - createLink: async ({ request, locals }) => { + createException: async ({ request, locals }) => { if (!locals.user || !locals.session) { return fail(401, { error: 'Unauthorized' }); } const formData = await request.formData(); + const userId = formData.get('userId') as string; const pathway = formData.get('pathway') as string; - const label = formData.get('label') as string | null; - const customSlug = (formData.get('slug') as string | null)?.trim() || null; + const weekNumber = parseInt(formData.get('weekNumber') as string, 10); + const reason = formData.get('reason') as string; + const expiresAt = formData.get('expiresAt') as string; + + if (!userId || !pathway || !weekNumber || !reason || !expiresAt) { + return fail(400, { error: 'All fields are required' }); + } - const validPathways = PATHWAY_IDS; - if (!pathway || !validPathways.includes(pathway)) { + if (!PATHWAY_IDS.includes(pathway)) { return fail(400, { error: 'Invalid pathway' }); } - let code: string; - if (customSlug) { - if (!/^[a-zA-Z0-9_-]{3,32}$/.test(customSlug)) { - return fail(400, { error: 'Slug must be 3-32 characters, letters, numbers, hyphens, or underscores only' }); - } - const existing = await db.query.referralLink.findFirst({ - where: eq(referralLink.code, customSlug) - }); - if (existing) { - return fail(400, { error: 'That slug is already taken' }); - } - code = customSlug; - } else { - code = generateReferralCode(); + const season = await db.query.programSeason.findFirst({ + where: eq(programSeason.isActive, true) + }); + + if (!season) { + return fail(500, { error: 'No active season' }); + } + + if (weekNumber < 1 || weekNumber > season.totalWeeks) { + return fail(400, { error: 'Invalid week number' }); } const assignment = await db.query.ambassadorPathway.findFirst({ @@ -98,65 +119,72 @@ export const actions: Actions = { return fail(403, { error: 'You are not assigned to this pathway' }); } - await db.insert(referralLink).values({ - ambassadorId: locals.user.id, - pathway: pathway as any, - code, - label: label || null - }); + try { + await db.insert(submissionClosureException).values({ + userId, + seasonId: season.id, + pathway: pathway as any, + weekNumber, + reason, + expiresAt: new Date(expiresAt), + createdBy: locals.user.id + }); + } catch { + return fail(400, { error: 'An exception already exists for this user, pathway, and week' }); + } return { success: true }; }, - toggleLink: async ({ request, locals }) => { + toggleException: async ({ request, locals }) => { if (!locals.user || !locals.session) { return fail(401, { error: 'Unauthorized' }); } const formData = await request.formData(); - const linkId = formData.get('linkId') as string; + const exceptionId = formData.get('exceptionId') as string; - if (!linkId) { - return fail(400, { error: 'Missing link ID' }); + if (!exceptionId) { + return fail(400, { error: 'Missing exception ID' }); } - const link = await db.query.referralLink.findFirst({ - where: and(eq(referralLink.id, linkId), eq(referralLink.ambassadorId, locals.user.id)) + const exception = await db.query.submissionClosureException.findFirst({ + where: eq(submissionClosureException.id, exceptionId) }); - if (!link) { - return fail(404, { error: 'Link not found' }); + if (!exception) { + return fail(404, { error: 'Exception not found' }); } await db - .update(referralLink) - .set({ isActive: !link.isActive }) - .where(eq(referralLink.id, linkId)); + .update(submissionClosureException) + .set({ isActive: !exception.isActive }) + .where(eq(submissionClosureException.id, exceptionId)); return { success: true }; }, - deleteLink: async ({ request, locals }) => { + deleteException: async ({ request, locals }) => { if (!locals.user || !locals.session) { return fail(401, { error: 'Unauthorized' }); } const formData = await request.formData(); - const linkId = formData.get('linkId') as string; + const exceptionId = formData.get('exceptionId') as string; - if (!linkId) { - return fail(400, { error: 'Missing link ID' }); + if (!exceptionId) { + return fail(400, { error: 'Missing exception ID' }); } - const link = await db.query.referralLink.findFirst({ - where: and(eq(referralLink.id, linkId), eq(referralLink.ambassadorId, locals.user.id)) + const exception = await db.query.submissionClosureException.findFirst({ + where: eq(submissionClosureException.id, exceptionId) }); - if (!link) { - return fail(404, { error: 'Link not found' }); + if (!exception) { + return fail(404, { error: 'Exception not found' }); } - await db.delete(referralLink).where(eq(referralLink.id, linkId)); + await db.delete(submissionClosureException).where(eq(submissionClosureException.id, exceptionId)); return { success: true }; } diff --git a/resolution-frontend/src/routes/app/ambassador/exceptions/+page.svelte b/resolution-frontend/src/routes/app/ambassador/exceptions/+page.svelte index b9a248d..554143a 100644 --- a/resolution-frontend/src/routes/app/ambassador/exceptions/+page.svelte +++ b/resolution-frontend/src/routes/app/ambassador/exceptions/+page.svelte @@ -2,40 +2,40 @@ import type { PageData } from './$types'; import PlatformBackground from '$lib/components/PlatformBackground.svelte'; import { enhance } from '$app/forms'; - import { page } from '$app/stores'; import { PATHWAY_INFO } from '$lib/pathways'; let { data }: { data: PageData } = $props(); + let searchQuery = $state(''); + let selectedUserId = $state(''); + let selectedPathway = $state(''); + let selectedWeek = $state(''); + let reason = $state(''); + let expiresAt = $state(''); + const pathwayInfo = PATHWAY_INFO; + const weeks = $derived(Array.from({ length: data.season.totalWeeks }, (_, i) => i + 1)); + + const filteredUsers = $derived( + searchQuery.length < 2 + ? [] + : data.enrolledUsers.filter( + (u) => + (u.firstName?.toLowerCase() || '').includes(searchQuery.toLowerCase()) || + (u.lastName?.toLowerCase() || '').includes(searchQuery.toLowerCase()) || + u.email.toLowerCase().includes(searchQuery.toLowerCase()) + ) + ); - let copiedId = $state(null); - let selectedPathway = $state(''); - let linkLabel = $state(''); - let linkSlug = $state(''); - let expandedLinks = $state>(new Set()); - - function copyLink(code: string, linkId: string) { - navigator.clipboard.writeText(`${$page.url.origin}/ref/${code}`); - copiedId = linkId; - setTimeout(() => { - copiedId = null; - }, 2000); - } - - function toggleExpanded(linkId: string) { - const next = new Set(expandedLinks); - if (next.has(linkId)) { - next.delete(linkId); - } else { - next.add(linkId); - } - expandedLinks = next; + function selectUser(userId: string, name: string) { + selectedUserId = userId; + searchQuery = name; } - function getLinksByPathway(pathway: string) { - return data.referralLinks.filter((l) => l.pathway === pathway); + function clearUser() { + selectedUserId = ''; + searchQuery = ''; } function formatDate(date: Date | string) { @@ -46,21 +46,17 @@ }); } - const pathwaysWithLinks = $derived( - data.assignments.filter((p) => getLinksByPathway(p).length > 0) - ); - - const pathwaysWithoutLinks = $derived( - data.assignments.filter((p) => getLinksByPathway(p).length === 0) + const canSubmit = $derived( + selectedUserId && selectedPathway && selectedWeek && reason && expiresAt ); - Referral Links - Resolution + Submission Exceptions - Resolution -
+
-

Referral Links

-

Create and manage referral links for your pathways

+

Submission Exceptions

+

Grant deadline extensions for {data.season.name}

- +
-

Create Referral Link

-
+

Create Exception

+ { + return async ({ update }) => { + await update(); + selectedUserId = ''; + searchQuery = ''; + selectedPathway = ''; + selectedWeek = ''; + reason = ''; + expiresAt = ''; + }; + }} + class="create-form" + > + +
+
+ +
+ { + if (selectedUserId) clearUser(); + }} + autocomplete="off" + /> + {#if selectedUserId} + + {/if} +
+ {#if searchQuery.length >= 2 && !selectedUserId} +
+ {#if filteredUsers.length === 0} +
No users found
+ {:else} + {#each filteredUsers.slice(0, 8) as u} + + {/each} + {/if} +
+ {/if} +
+
+
- + + +
+
+ +
+
+ - /ref/{linkSlug || 'auto-generated'}
- +
-
- - {#if data.referralLinks.length === 0} + + {#if data.exceptions.length === 0}
-

You haven't created any referral links yet.

-

Use the form above to create your first link.

+

No exceptions have been created yet.

+

Use the form above to grant a deadline extension.

{:else} - {#each pathwaysWithLinks as pathway} - {@const info = pathwayInfo[pathway]} - {@const links = getLinksByPathway(pathway)} -
-
- {info.label} -

{info.label}

- {links.length} link{links.length !== 1 ? 's' : ''} -
- -
{/if}
diff --git a/resolution-frontend/src/routes/app/pathway/[pathway]/week/[week]/+page.server.ts b/resolution-frontend/src/routes/app/pathway/[pathway]/week/[week]/+page.server.ts index f89b5ce..334fea9 100644 --- a/resolution-frontend/src/routes/app/pathway/[pathway]/week/[week]/+page.server.ts +++ b/resolution-frontend/src/routes/app/pathway/[pathway]/week/[week]/+page.server.ts @@ -4,6 +4,7 @@ import { userPathway, pathwayWeekContent } from '$lib/server/db/schema'; import { eq, and } from 'drizzle-orm'; import { redirect, error } from '@sveltejs/kit'; import { PATHWAY_IDS, type PathwayId } from '$lib/pathways'; +import { ExceptionService } from '$lib/server/services'; const validPathways = PATHWAY_IDS; type Pathway = PathwayId; @@ -41,11 +42,17 @@ export const load: PageServerLoad = async ({ params, parent }) => { throw error(404, 'This week is not yet available'); } + let exception: { expiresAt: Date } | null = null; + if (!content.isSubmissionsOpen) { + exception = await ExceptionService.getActiveException(user.id, pathwayId, weekNumber); + } + return { pathwayId, weekNumber, title: content.title, content: content.content, - isSubmissionsOpen: content.isSubmissionsOpen + isSubmissionsOpen: content.isSubmissionsOpen || !!exception, + exception: exception ? { expiresAt: exception.expiresAt.toISOString() } : null }; }; diff --git a/resolution-frontend/src/routes/app/pathway/[pathway]/week/[week]/+page.svelte b/resolution-frontend/src/routes/app/pathway/[pathway]/week/[week]/+page.svelte index 6613da5..8d0ea04 100644 --- a/resolution-frontend/src/routes/app/pathway/[pathway]/week/[week]/+page.svelte +++ b/resolution-frontend/src/routes/app/pathway/[pathway]/week/[week]/+page.svelte @@ -17,6 +17,12 @@

Ready to ship?

{#if data.isSubmissionsOpen} + {#if data.exception} +
+ + You have a deadline extension. Submit by {new Date(data.exception.expiresAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })} +
+ {/if}

Finished your project for this week? Submit it to earn rewards!

Ship Project @@ -84,4 +90,17 @@ .ship-btn-disabled:hover { background: #b5bfcc; } + + .exception-notice { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + background: #fff3e0; + border: 1px solid #ff8c37; + border-radius: 10px; + font-size: 0.85rem; + color: #1a1a2e; + margin-bottom: 0.75rem; + } diff --git a/resolution-frontend/src/routes/app/pathway/[pathway]/week/[week]/ship/+page.server.ts b/resolution-frontend/src/routes/app/pathway/[pathway]/week/[week]/ship/+page.server.ts index 186d85f..93c524e 100644 --- a/resolution-frontend/src/routes/app/pathway/[pathway]/week/[week]/ship/+page.server.ts +++ b/resolution-frontend/src/routes/app/pathway/[pathway]/week/[week]/ship/+page.server.ts @@ -4,6 +4,7 @@ import { userPathway, pathwayWeekContent } from '$lib/server/db/schema'; import { eq, and } from 'drizzle-orm'; import { redirect, error } from '@sveltejs/kit'; import { PATHWAY_IDS, type PathwayId } from '$lib/pathways'; +import { ExceptionService } from '$lib/server/services'; export const load: PageServerLoad = async ({ params, parent }) => { const { user } = await parent(); @@ -44,7 +45,10 @@ export const load: PageServerLoad = async ({ params, parent }) => { } if (!content[0].isSubmissionsOpen) { - throw error(403, 'Submissions have been closed for this week'); + const exception = await ExceptionService.getActiveException(user.id, pathwayId, weekNumber); + if (!exception) { + throw error(403, 'Submissions have been closed for this week'); + } } return { From 41b935d5a1b072298b5da27c6b6dc1ecde613486 Mon Sep 17 00:00:00 2001 From: Niko Yu <137782638+thesleepyniko@users.noreply.github.com> Date: Mon, 20 Apr 2026 23:08:20 -0400 Subject: [PATCH 07/20] test: tests for exception service --- .../server/services/exceptionService.test.ts | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 resolution-frontend/src/lib/server/services/exceptionService.test.ts diff --git a/resolution-frontend/src/lib/server/services/exceptionService.test.ts b/resolution-frontend/src/lib/server/services/exceptionService.test.ts new file mode 100644 index 0000000..1834462 --- /dev/null +++ b/resolution-frontend/src/lib/server/services/exceptionService.test.ts @@ -0,0 +1,71 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +const mockFindFirstSeason = vi.fn(); +const mockSelect = vi.fn(); +const mockFrom = vi.fn(); +const mockWhere = vi.fn(); +const mockLimit = vi.fn(); + +vi.mock('../db', () => ({ + db: { + query: { + programSeason: { findFirst: (...args: unknown[]) => mockFindFirstSeason(...args) } + }, + select: (...args: unknown[]) => { + mockSelect(...args); + return { from: (...a: unknown[]) => { mockFrom(...a); return { where: (...w: unknown[]) => { mockWhere(...w); return { limit: (...l: unknown[]) => mockLimit(...l) }; } }; } }; + } + } +})); + +const { ExceptionService } = await import('./exceptionService'); + +beforeEach(() => { + vi.clearAllMocks(); +}); + +describe('ExceptionService.getActiveException', () => { + it('returns null when no active season exists', async () => { + mockFindFirstSeason.mockResolvedValue(undefined); + + const result = await ExceptionService.getActiveException('user-1', 'PYTHON', 1); + expect(result).toBeNull(); + expect(mockSelect).not.toHaveBeenCalled(); + }); + + it('returns null when no exception found', async () => { + mockFindFirstSeason.mockResolvedValue({ id: 'season-1', isActive: true }); + mockLimit.mockResolvedValue([]); + + const result = await ExceptionService.getActiveException('user-1', 'PYTHON', 1); + expect(result).toBeNull(); + }); + + it('returns exception when a valid one exists', async () => { + const exception = { id: 'exc-1', expiresAt: new Date('2026-06-01') }; + mockFindFirstSeason.mockResolvedValue({ id: 'season-1', isActive: true }); + mockLimit.mockResolvedValue([exception]); + + const result = await ExceptionService.getActiveException('user-1', 'PYTHON', 1); + expect(result).toEqual(exception); + }); + + it('queries with correct season id', async () => { + mockFindFirstSeason.mockResolvedValue({ id: 'season-42', isActive: true }); + mockLimit.mockResolvedValue([]); + + await ExceptionService.getActiveException('user-1', 'RUST', 3); + + expect(mockFindFirstSeason).toHaveBeenCalledTimes(1); + expect(mockSelect).toHaveBeenCalledTimes(1); + expect(mockLimit).toHaveBeenCalledWith(1); + }); + + it('returns null for empty array result', async () => { + mockFindFirstSeason.mockResolvedValue({ id: 'season-1', isActive: true }); + mockLimit.mockResolvedValue([]); + + const result = await ExceptionService.getActiveException('user-1', 'GAME_DEV', 5); + expect(result).toBeNull(); + }); +}); From d9814b3e5eb53397c7e766b5f833dbffa3b37977 Mon Sep 17 00:00:00 2001 From: Niko Yu <137782638+thesleepyniko@users.noreply.github.com> Date: Mon, 20 Apr 2026 23:15:57 -0400 Subject: [PATCH 08/20] style: add semicolon --- resolution-frontend/src/lib/server/db/schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resolution-frontend/src/lib/server/db/schema.ts b/resolution-frontend/src/lib/server/db/schema.ts index 2161ff4..f19b359 100644 --- a/resolution-frontend/src/lib/server/db/schema.ts +++ b/resolution-frontend/src/lib/server/db/schema.ts @@ -244,7 +244,7 @@ export const submissionClosureExceptionRelations = relations(submissionClosureEx user: one(user, { fields: [submissionClosureException.userId], references: [user.id] }), createdBy: one(user, { fields: [submissionClosureException.createdBy], references: [user.id] }), season: one(programSeason, { fields: [submissionClosureException.seasonId], references: [programSeason.id]}) -})) +})); // Ambassador pathway assignments - which pathways an ambassador can edit export const ambassadorPathway = pgTable('ambassador_pathway', { From 0f8be92a43842cab16a9a78c00378cd433533b00 Mon Sep 17 00:00:00 2001 From: Niko Yu <137782638+thesleepyniko@users.noreply.github.com> Date: Mon, 20 Apr 2026 23:17:58 -0400 Subject: [PATCH 09/20] fix: prevent ambassadors from viewing exceptions for unassigned pathways --- .../src/routes/app/ambassador/exceptions/+page.server.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/resolution-frontend/src/routes/app/ambassador/exceptions/+page.server.ts b/resolution-frontend/src/routes/app/ambassador/exceptions/+page.server.ts index 074d43c..f529cf1 100644 --- a/resolution-frontend/src/routes/app/ambassador/exceptions/+page.server.ts +++ b/resolution-frontend/src/routes/app/ambassador/exceptions/+page.server.ts @@ -60,7 +60,14 @@ export const load: PageServerLoad = async ({ parent }) => { }) .from(submissionClosureException) .innerJoin(user, eq(submissionClosureException.userId, user.id)) - .where(eq(submissionClosureException.seasonId, season.id)) + .where( + currentUser.isAdmin + ? eq(submissionClosureException.seasonId, season.id) + : and( + eq(submissionClosureException.seasonId, season.id), + inArray(submissionClosureException.pathway, assignedPathways) + ) + ) ]); return { From 84465e40b018b841d3c373365420dda0e7de443a Mon Sep 17 00:00:00 2001 From: Niko Yu <137782638+thesleepyniko@users.noreply.github.com> Date: Mon, 20 Apr 2026 23:19:49 -0400 Subject: [PATCH 10/20] fix: ensure space between first and last name --- .../src/routes/app/ambassador/exceptions/+page.svelte | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/resolution-frontend/src/routes/app/ambassador/exceptions/+page.svelte b/resolution-frontend/src/routes/app/ambassador/exceptions/+page.svelte index 554143a..d9aff4d 100644 --- a/resolution-frontend/src/routes/app/ambassador/exceptions/+page.svelte +++ b/resolution-frontend/src/routes/app/ambassador/exceptions/+page.svelte @@ -128,8 +128,7 @@ )} > - {u.firstName || ''} - {u.lastName || ''} + {[u.firstName, u.lastName].filter(Boolean).join(' ') || u.email} {u.email} From d98719aea3b69e42acbe387f37761ddf115cbbc8 Mon Sep 17 00:00:00 2001 From: Niko Yu <137782638+thesleepyniko@users.noreply.github.com> Date: Mon, 20 Apr 2026 23:20:38 -0400 Subject: [PATCH 11/20] fix: render name with space --- .../src/routes/app/ambassador/exceptions/+page.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resolution-frontend/src/routes/app/ambassador/exceptions/+page.svelte b/resolution-frontend/src/routes/app/ambassador/exceptions/+page.svelte index d9aff4d..12e762d 100644 --- a/resolution-frontend/src/routes/app/ambassador/exceptions/+page.svelte +++ b/resolution-frontend/src/routes/app/ambassador/exceptions/+page.svelte @@ -216,8 +216,8 @@
- {exception.userName || ''} - {exception.userLastName || ''} + {[exception.userName, exception.userLastName].filter(Boolean).join(' ')} + {exception.userEmail}
From e8b7f67cf395025da00c73b0a50d1c2fe3294a6a Mon Sep 17 00:00:00 2001 From: Niko Yu <137782638+thesleepyniko@users.noreply.github.com> Date: Mon, 20 Apr 2026 23:34:38 -0400 Subject: [PATCH 12/20] fix: update exception expiration handling to use date type and adjust related logic --- resolution-frontend/src/lib/server/db/schema.ts | 4 ++-- .../src/lib/server/services/exceptionService.test.ts | 2 +- .../src/lib/server/services/exceptionService.ts | 4 ++-- .../src/routes/app/ambassador/exceptions/+page.server.ts | 2 +- .../routes/app/pathway/[pathway]/week/[week]/+page.server.ts | 4 ++-- .../src/routes/app/pathway/[pathway]/week/[week]/+page.svelte | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/resolution-frontend/src/lib/server/db/schema.ts b/resolution-frontend/src/lib/server/db/schema.ts index f19b359..842cdd1 100644 --- a/resolution-frontend/src/lib/server/db/schema.ts +++ b/resolution-frontend/src/lib/server/db/schema.ts @@ -1,4 +1,4 @@ -import { pgTable, text, timestamp, boolean, integer, real, pgEnum, uniqueIndex, index } from 'drizzle-orm/pg-core'; +import { pgTable, text, timestamp, boolean, integer, real, pgEnum, uniqueIndex, index, date } from 'drizzle-orm/pg-core'; import { relations } from 'drizzle-orm'; import { createId } from '@paralleldrive/cuid2'; @@ -147,7 +147,7 @@ export const submissionClosureException = pgTable('submission_closure_exception' weekNumber: integer('week_number').notNull(), reason: text('reason').notNull(), isActive: boolean('is_active').notNull().default(true), - expiresAt: timestamp('expires_at', { mode: 'date' }).notNull(), + expiresAt: date('expires_at').notNull(), // we only care abt date + want to avoid timezone issues, date returns str vs Date obj createdAt: timestamp('created_at', { mode: 'date' }).notNull().defaultNow(), createdBy: text('created_by').notNull().references(() => user.id, { onDelete: 'cascade' }) }, (table) => [ diff --git a/resolution-frontend/src/lib/server/services/exceptionService.test.ts b/resolution-frontend/src/lib/server/services/exceptionService.test.ts index 1834462..16035c0 100644 --- a/resolution-frontend/src/lib/server/services/exceptionService.test.ts +++ b/resolution-frontend/src/lib/server/services/exceptionService.test.ts @@ -42,7 +42,7 @@ describe('ExceptionService.getActiveException', () => { }); it('returns exception when a valid one exists', async () => { - const exception = { id: 'exc-1', expiresAt: new Date('2026-06-01') }; + const exception = { id: 'exc-1', expiresAt: '2026-06-01' }; mockFindFirstSeason.mockResolvedValue({ id: 'season-1', isActive: true }); mockLimit.mockResolvedValue([exception]); diff --git a/resolution-frontend/src/lib/server/services/exceptionService.ts b/resolution-frontend/src/lib/server/services/exceptionService.ts index e2a884e..71b3ab2 100644 --- a/resolution-frontend/src/lib/server/services/exceptionService.ts +++ b/resolution-frontend/src/lib/server/services/exceptionService.ts @@ -1,6 +1,6 @@ import { db } from '../db'; import { submissionClosureException, programSeason } from '../db/schema'; -import { eq, and, gt } from 'drizzle-orm'; +import { eq, and, gte, sql } from 'drizzle-orm'; import type { PathwayId } from '$lib/pathways'; export const ExceptionService = { @@ -32,7 +32,7 @@ export const ExceptionService = { eq(submissionClosureException.pathway, pathway), eq(submissionClosureException.weekNumber, weekNumber), eq(submissionClosureException.isActive, true), - gt(submissionClosureException.expiresAt, new Date()) + gte(submissionClosureException.expiresAt, sql`CURRENT_DATE`) ) ) .limit(1); diff --git a/resolution-frontend/src/routes/app/ambassador/exceptions/+page.server.ts b/resolution-frontend/src/routes/app/ambassador/exceptions/+page.server.ts index f529cf1..3741ffb 100644 --- a/resolution-frontend/src/routes/app/ambassador/exceptions/+page.server.ts +++ b/resolution-frontend/src/routes/app/ambassador/exceptions/+page.server.ts @@ -133,7 +133,7 @@ export const actions: Actions = { pathway: pathway as any, weekNumber, reason, - expiresAt: new Date(expiresAt), + expiresAt, createdBy: locals.user.id }); } catch { diff --git a/resolution-frontend/src/routes/app/pathway/[pathway]/week/[week]/+page.server.ts b/resolution-frontend/src/routes/app/pathway/[pathway]/week/[week]/+page.server.ts index 334fea9..8e0dd38 100644 --- a/resolution-frontend/src/routes/app/pathway/[pathway]/week/[week]/+page.server.ts +++ b/resolution-frontend/src/routes/app/pathway/[pathway]/week/[week]/+page.server.ts @@ -42,7 +42,7 @@ export const load: PageServerLoad = async ({ params, parent }) => { throw error(404, 'This week is not yet available'); } - let exception: { expiresAt: Date } | null = null; + let exception: { expiresAt: string } | null = null; if (!content.isSubmissionsOpen) { exception = await ExceptionService.getActiveException(user.id, pathwayId, weekNumber); } @@ -53,6 +53,6 @@ export const load: PageServerLoad = async ({ params, parent }) => { title: content.title, content: content.content, isSubmissionsOpen: content.isSubmissionsOpen || !!exception, - exception: exception ? { expiresAt: exception.expiresAt.toISOString() } : null + exception }; }; diff --git a/resolution-frontend/src/routes/app/pathway/[pathway]/week/[week]/+page.svelte b/resolution-frontend/src/routes/app/pathway/[pathway]/week/[week]/+page.svelte index 8d0ea04..f20e589 100644 --- a/resolution-frontend/src/routes/app/pathway/[pathway]/week/[week]/+page.svelte +++ b/resolution-frontend/src/routes/app/pathway/[pathway]/week/[week]/+page.svelte @@ -20,7 +20,7 @@ {#if data.exception}
- You have a deadline extension. Submit by {new Date(data.exception.expiresAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })} + You have a deadline extension. Submit by {new Date(data.exception.expiresAt + 'T00:00:00').toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
{/if}

Finished your project for this week? Submit it to earn rewards!

From 544598d3b4eddb6a8b9893db0b52b5d42b6b921b Mon Sep 17 00:00:00 2001 From: Niko Yu <137782638+thesleepyniko@users.noreply.github.com> Date: Mon, 20 Apr 2026 23:37:08 -0400 Subject: [PATCH 13/20] fix: db FK stuff --- .../drizzle/0006_happy_glorian.sql | 1 + .../drizzle/meta/0006_snapshot.json | 1816 +++++++++++++++++ .../drizzle/meta/_journal.json | 7 + .../src/lib/server/db/schema.ts | 7 +- 4 files changed, 1828 insertions(+), 3 deletions(-) create mode 100644 resolution-frontend/drizzle/0006_happy_glorian.sql create mode 100644 resolution-frontend/drizzle/meta/0006_snapshot.json diff --git a/resolution-frontend/drizzle/0006_happy_glorian.sql b/resolution-frontend/drizzle/0006_happy_glorian.sql new file mode 100644 index 0000000..2208666 --- /dev/null +++ b/resolution-frontend/drizzle/0006_happy_glorian.sql @@ -0,0 +1 @@ +ALTER TABLE "submission_closure_exception" ALTER COLUMN "expires_at" SET DATA TYPE date; \ No newline at end of file diff --git a/resolution-frontend/drizzle/meta/0006_snapshot.json b/resolution-frontend/drizzle/meta/0006_snapshot.json new file mode 100644 index 0000000..2cabbd7 --- /dev/null +++ b/resolution-frontend/drizzle/meta/0006_snapshot.json @@ -0,0 +1,1816 @@ +{ + "id": "bd8506d4-a303-4b90-a2a3-175fe787468c", + "prevId": "097f7629-1e63-459c-803a-0dcbfbca8123", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.ambassador_pathway": { + "name": "ambassador_pathway", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pathway": { + "name": "pathway", + "type": "pathway", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "ambassador_pathway_unique_idx": { + "name": "ambassador_pathway_unique_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pathway", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ambassador_pathway_user_id_user_id_fk": { + "name": "ambassador_pathway_user_id_user_id_fk", + "tableFrom": "ambassador_pathway", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ambassador_pathway_assigned_by_user_id_fk": { + "name": "ambassador_pathway_assigned_by_user_id_fk", + "tableFrom": "ambassador_pathway", + "tableTo": "user", + "columnsFrom": [ + "assigned_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ambassador_payout": { + "name": "ambassador_payout", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ambassador_id": { + "name": "ambassador_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "season_id": { + "name": "season_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_cents": { + "name": "amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "payout_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'DRAFT'" + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "paid_at": { + "name": "paid_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "ambassador_payout_ambassador_id_user_id_fk": { + "name": "ambassador_payout_ambassador_id_user_id_fk", + "tableFrom": "ambassador_payout", + "tableTo": "user", + "columnsFrom": [ + "ambassador_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ambassador_payout_season_id_program_season_id_fk": { + "name": "ambassador_payout_season_id_program_season_id_fk", + "tableFrom": "ambassador_payout", + "tableTo": "program_season", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ambassador_payout_item": { + "name": "ambassador_payout_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "payout_id": { + "name": "payout_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workshop_id": { + "name": "workshop_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "completion_count": { + "name": "completion_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "rate_cents_per_completion": { + "name": "rate_cents_per_completion", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "amount_cents": { + "name": "amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "ambassador_payout_item_payout_id_ambassador_payout_id_fk": { + "name": "ambassador_payout_item_payout_id_ambassador_payout_id_fk", + "tableFrom": "ambassador_payout_item", + "tableTo": "ambassador_payout", + "columnsFrom": [ + "payout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ambassador_payout_item_workshop_id_workshop_id_fk": { + "name": "ambassador_payout_item_workshop_id_workshop_id_fk", + "tableFrom": "ambassador_payout_item", + "tableTo": "workshop", + "columnsFrom": [ + "workshop_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pathway_week_content": { + "name": "pathway_week_content", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "pathway": { + "name": "pathway", + "type": "pathway", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "week_number": { + "name": "week_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "prize_image_url": { + "name": "prize_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_submissions_open": { + "name": "is_submissions_open", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_edited_by": { + "name": "last_edited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pathway_week_content_unique_idx": { + "name": "pathway_week_content_unique_idx", + "columns": [ + { + "expression": "pathway", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "week_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pathway_week_content_last_edited_by_user_id_fk": { + "name": "pathway_week_content_last_edited_by_user_id_fk", + "tableFrom": "pathway_week_content", + "tableTo": "user", + "columnsFrom": [ + "last_edited_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.program_enrollment": { + "name": "program_enrollment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "season_id": { + "name": "season_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "enrollment_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "enrollment_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ACTIVE'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "starting_week": { + "name": "starting_week", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "enrollment_user_season_role_idx": { + "name": "enrollment_user_season_role_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "program_enrollment_user_id_user_id_fk": { + "name": "program_enrollment_user_id_user_id_fk", + "tableFrom": "program_enrollment", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "program_enrollment_season_id_program_season_id_fk": { + "name": "program_enrollment_season_id_program_season_id_fk", + "tableFrom": "program_enrollment", + "tableTo": "program_season", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.program_season": { + "name": "program_season", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signup_opens_at": { + "name": "signup_opens_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "signup_closes_at": { + "name": "signup_closes_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "starts_at": { + "name": "starts_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ends_at": { + "name": "ends_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "total_weeks": { + "name": "total_weeks", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 8 + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "program_season_slug_unique": { + "name": "program_season_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.referral_link": { + "name": "referral_link", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ambassador_id": { + "name": "ambassador_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pathway": { + "name": "pathway", + "type": "pathway", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "referral_link_ambassador_id_user_id_fk": { + "name": "referral_link_ambassador_id_user_id_fk", + "tableFrom": "referral_link", + "tableTo": "user", + "columnsFrom": [ + "ambassador_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "referral_link_code_unique": { + "name": "referral_link_code_unique", + "nullsNotDistinct": false, + "columns": [ + "code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.referral_signup": { + "name": "referral_signup", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "referral_link_id": { + "name": "referral_link_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "referral_signup_unique_idx": { + "name": "referral_signup_unique_idx", + "columns": [ + { + "expression": "referral_link_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "referral_signup_referral_link_id_referral_link_id_fk": { + "name": "referral_signup_referral_link_id_referral_link_id_fk", + "tableFrom": "referral_signup", + "tableTo": "referral_link", + "columnsFrom": [ + "referral_link_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "referral_signup_user_id_user_id_fk": { + "name": "referral_signup_user_id_user_id_fk", + "tableFrom": "referral_signup", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reviewer_pathway": { + "name": "reviewer_pathway", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pathway": { + "name": "pathway", + "type": "pathway", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "reviewer_pathway_unique_idx": { + "name": "reviewer_pathway_unique_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pathway", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reviewer_pathway_user_id_user_id_fk": { + "name": "reviewer_pathway_user_id_user_id_fk", + "tableFrom": "reviewer_pathway", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "reviewer_pathway_assigned_by_user_id_fk": { + "name": "reviewer_pathway_assigned_by_user_id_fk", + "tableFrom": "reviewer_pathway", + "tableTo": "user", + "columnsFrom": [ + "assigned_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.submission_closure_exception": { + "name": "submission_closure_exception", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "season_id": { + "name": "season_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pathway": { + "name": "pathway", + "type": "pathway", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "week_number": { + "name": "week_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "expires_at": { + "name": "expires_at", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "submission_exception_unique_idx": { + "name": "submission_exception_unique_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pathway", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "week_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "submission_exception_lookup_idx": { + "name": "submission_exception_lookup_idx", + "columns": [ + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pathway", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "week_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "submission_exception_user_idx": { + "name": "submission_exception_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "submission_closure_exception_user_id_user_id_fk": { + "name": "submission_closure_exception_user_id_user_id_fk", + "tableFrom": "submission_closure_exception", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "submission_closure_exception_season_id_program_season_id_fk": { + "name": "submission_closure_exception_season_id_program_season_id_fk", + "tableFrom": "submission_closure_exception", + "tableTo": "program_season", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "submission_closure_exception_created_by_user_id_fk": { + "name": "submission_closure_exception_created_by_user_id_fk", + "tableFrom": "submission_closure_exception", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hack_club_id": { + "name": "hack_club_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slack_id": { + "name": "slack_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verification_status": { + "name": "verification_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ysws_eligible": { + "name": "ysws_eligible", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "user_hack_club_id_unique": { + "name": "user_hack_club_id_unique", + "nullsNotDistinct": false, + "columns": [ + "hack_club_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_pathway": { + "name": "user_pathway", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pathway": { + "name": "pathway", + "type": "pathway", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_pathway_unique_idx": { + "name": "user_pathway_unique_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pathway", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_pathway_user_id_user_id_fk": { + "name": "user_pathway_user_id_user_id_fk", + "tableFrom": "user_pathway", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.weekly_ship": { + "name": "weekly_ship", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "season_id": { + "name": "season_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workshop_id": { + "name": "workshop_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "week_number": { + "name": "week_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "goal_text": { + "name": "goal_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "ship_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'PLANNED'" + }, + "proof_url": { + "name": "proof_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shipped_at": { + "name": "shipped_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ship_user_season_week_idx": { + "name": "ship_user_season_week_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "week_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "weekly_ship_user_id_user_id_fk": { + "name": "weekly_ship_user_id_user_id_fk", + "tableFrom": "weekly_ship", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "weekly_ship_season_id_program_season_id_fk": { + "name": "weekly_ship_season_id_program_season_id_fk", + "tableFrom": "weekly_ship", + "tableTo": "program_season", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "weekly_ship_workshop_id_workshop_id_fk": { + "name": "weekly_ship_workshop_id_workshop_id_fk", + "tableFrom": "weekly_ship", + "tableTo": "workshop", + "columnsFrom": [ + "workshop_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workshop": { + "name": "workshop", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "season_id": { + "name": "season_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pathway": { + "name": "pathway", + "type": "pathway", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "difficulty": { + "name": "difficulty", + "type": "difficulty", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "estimated_hours": { + "name": "estimated_hours", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "published": { + "name": "published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workshop_author_id_user_id_fk": { + "name": "workshop_author_id_user_id_fk", + "tableFrom": "workshop", + "tableTo": "user", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workshop_season_id_program_season_id_fk": { + "name": "workshop_season_id_program_season_id_fk", + "tableFrom": "workshop", + "tableTo": "program_season", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workshop_analytics": { + "name": "workshop_analytics", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workshop_id": { + "name": "workshop_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "starts": { + "name": "starts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "completions": { + "name": "completions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "avg_completion_mins": { + "name": "avg_completion_mins", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workshop_analytics_workshop_id_workshop_id_fk": { + "name": "workshop_analytics_workshop_id_workshop_id_fk", + "tableFrom": "workshop_analytics", + "tableTo": "workshop", + "columnsFrom": [ + "workshop_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workshop_analytics_workshop_id_unique": { + "name": "workshop_analytics_workshop_id_unique", + "nullsNotDistinct": false, + "columns": [ + "workshop_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workshop_completion": { + "name": "workshop_completion", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workshop_id": { + "name": "workshop_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "participant_id": { + "name": "participant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "season_id": { + "name": "season_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_url": { + "name": "project_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "completion_workshop_participant_season_idx": { + "name": "completion_workshop_participant_season_idx", + "columns": [ + { + "expression": "workshop_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workshop_completion_workshop_id_workshop_id_fk": { + "name": "workshop_completion_workshop_id_workshop_id_fk", + "tableFrom": "workshop_completion", + "tableTo": "workshop", + "columnsFrom": [ + "workshop_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workshop_completion_participant_id_user_id_fk": { + "name": "workshop_completion_participant_id_user_id_fk", + "tableFrom": "workshop_completion", + "tableTo": "user", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workshop_completion_season_id_program_season_id_fk": { + "name": "workshop_completion_season_id_program_season_id_fk", + "tableFrom": "workshop_completion", + "tableTo": "program_season", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.difficulty": { + "name": "difficulty", + "schema": "public", + "values": [ + "BEGINNER", + "INTERMEDIATE", + "ADVANCED" + ] + }, + "public.enrollment_role": { + "name": "enrollment_role", + "schema": "public", + "values": [ + "PARTICIPANT", + "AMBASSADOR" + ] + }, + "public.enrollment_status": { + "name": "enrollment_status", + "schema": "public", + "values": [ + "ACTIVE", + "DROPPED", + "COMPLETED" + ] + }, + "public.pathway": { + "name": "pathway", + "schema": "public", + "values": [ + "PYTHON", + "RUST", + "GAME_DEV", + "HARDWARE", + "DESIGN", + "GENERAL_CODING" + ] + }, + "public.payout_status": { + "name": "payout_status", + "schema": "public", + "values": [ + "DRAFT", + "PENDING", + "PAID", + "CANCELED" + ] + }, + "public.ship_status": { + "name": "ship_status", + "schema": "public", + "values": [ + "PLANNED", + "IN_PROGRESS", + "SHIPPED", + "MISSED" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/resolution-frontend/drizzle/meta/_journal.json b/resolution-frontend/drizzle/meta/_journal.json index 3412586..7a5edef 100644 --- a/resolution-frontend/drizzle/meta/_journal.json +++ b/resolution-frontend/drizzle/meta/_journal.json @@ -43,6 +43,13 @@ "when": 1776740262163, "tag": "0005_productive_madripoor", "breakpoints": true + }, + { + "idx": 6, + "version": "7", + "when": 1776742511832, + "tag": "0006_happy_glorian", + "breakpoints": true } ] } \ No newline at end of file diff --git a/resolution-frontend/src/lib/server/db/schema.ts b/resolution-frontend/src/lib/server/db/schema.ts index 842cdd1..5fa715f 100644 --- a/resolution-frontend/src/lib/server/db/schema.ts +++ b/resolution-frontend/src/lib/server/db/schema.ts @@ -179,7 +179,8 @@ export const userRelations = relations(user, ({ many }) => ({ payouts: many(ambassadorPayout), referralLinks: many(referralLink), reviewerAssignments: many(reviewerPathway), - exceptions: many(submissionClosureException) + exceptions: many(submissionClosureException, { relationName: 'exceptionUser' }), + createdExceptions: many(submissionClosureException, { relationName: 'exceptionCreator' }) })); export const sessionRelations = relations(session, ({ one }) => ({ @@ -241,8 +242,8 @@ export const userPathwayRelations = relations(userPathway, ({ one }) => ({ })); export const submissionClosureExceptionRelations = relations(submissionClosureException, ({ one }) => ({ - user: one(user, { fields: [submissionClosureException.userId], references: [user.id] }), - createdBy: one(user, { fields: [submissionClosureException.createdBy], references: [user.id] }), + user: one(user, { fields: [submissionClosureException.userId], references: [user.id], relationName: 'exceptionUser' }), + creator: one(user, { fields: [submissionClosureException.createdBy], references: [user.id], relationName: 'exceptionCreator' }), season: one(programSeason, { fields: [submissionClosureException.seasonId], references: [programSeason.id]}) })); From c68a44a0a764c3835c8c397d974de13f5781915a Mon Sep 17 00:00:00 2001 From: Niko Yu <137782638+thesleepyniko@users.noreply.github.com> Date: Mon, 20 Apr 2026 23:37:15 -0400 Subject: [PATCH 14/20] fix: add pathway assignment check for non-admin users in exception actions --- .../app/ambassador/exceptions/+page.server.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/resolution-frontend/src/routes/app/ambassador/exceptions/+page.server.ts b/resolution-frontend/src/routes/app/ambassador/exceptions/+page.server.ts index 3741ffb..e5c6a23 100644 --- a/resolution-frontend/src/routes/app/ambassador/exceptions/+page.server.ts +++ b/resolution-frontend/src/routes/app/ambassador/exceptions/+page.server.ts @@ -163,6 +163,18 @@ export const actions: Actions = { return fail(404, { error: 'Exception not found' }); } + if (!locals.user.isAdmin) { + const assignment = await db.query.ambassadorPathway.findFirst({ + where: and( + eq(ambassadorPathway.userId, locals.user.id), + eq(ambassadorPathway.pathway, exception.pathway) + ) + }); + if (!assignment) { + return fail(403, { error: 'You are not assigned to this pathway' }); + } + } + await db .update(submissionClosureException) .set({ isActive: !exception.isActive }) @@ -191,6 +203,18 @@ export const actions: Actions = { return fail(404, { error: 'Exception not found' }); } + if (!locals.user.isAdmin) { + const assignment = await db.query.ambassadorPathway.findFirst({ + where: and( + eq(ambassadorPathway.userId, locals.user.id), + eq(ambassadorPathway.pathway, exception.pathway) + ) + }); + if (!assignment) { + return fail(403, { error: 'You are not assigned to this pathway' }); + } + } + await db.delete(submissionClosureException).where(eq(submissionClosureException.id, exceptionId)); return { success: true }; From f75f5eb7b615d3888b7e33c55ca05d53b6171927 Mon Sep 17 00:00:00 2001 From: Niko Yu <137782638+thesleepyniko@users.noreply.github.com> Date: Tue, 21 Apr 2026 00:11:35 -0400 Subject: [PATCH 15/20] fix: increase accessibility Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/routes/app/ambassador/exceptions/+page.svelte | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/resolution-frontend/src/routes/app/ambassador/exceptions/+page.svelte b/resolution-frontend/src/routes/app/ambassador/exceptions/+page.svelte index 12e762d..a2bbd45 100644 --- a/resolution-frontend/src/routes/app/ambassador/exceptions/+page.svelte +++ b/resolution-frontend/src/routes/app/ambassador/exceptions/+page.svelte @@ -108,7 +108,15 @@ autocomplete="off" /> {#if selectedUserId} - + {/if}
{#if searchQuery.length >= 2 && !selectedUserId} From 4442f7931ac1a646e74a9011dc07f158deef5646 Mon Sep 17 00:00:00 2001 From: Niko Yu <137782638+thesleepyniko@users.noreply.github.com> Date: Tue, 28 Apr 2026 18:06:17 -0400 Subject: [PATCH 16/20] feat: add slack ID for submissions --- .../routes/api/review/submissions/+server.ts | 59 +++++++++++++------ .../src/routes/app/reviewer/+page.svelte | 20 +++++++ 2 files changed, 61 insertions(+), 18 deletions(-) diff --git a/resolution-frontend/src/routes/api/review/submissions/+server.ts b/resolution-frontend/src/routes/api/review/submissions/+server.ts index 702fb52..46cdccc 100644 --- a/resolution-frontend/src/routes/api/review/submissions/+server.ts +++ b/resolution-frontend/src/routes/api/review/submissions/+server.ts @@ -4,8 +4,8 @@ import { json } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; import { requireAuth } from '$lib/server/auth/guard'; import { db } from '$lib/server/db'; -import { reviewerPathway } from '$lib/server/db/schema'; -import { eq } from 'drizzle-orm'; +import { reviewerPathway, user as userTable } from '$lib/server/db/schema'; +import { eq, inArray } from 'drizzle-orm'; import { PATHWAY_IDS } from '$lib/pathways'; export const GET: RequestHandler = async (event) => { @@ -71,22 +71,45 @@ export const GET: RequestHandler = async (event) => { }) .all(); - const submissions = records.map((record) => ({ - id: record.id, - codeUrl: record.get('Code URL') as string, - playableUrl: record.get('Playable URL') as string, - description: record.get('Description') as string, - firstName: record.get('First Name') as string, - lastName: record.get('Last Name') as string, - email: record.get('Email') as string, - githubUsername: record.get('GitHub Username') as string, - hackatimeProject: record.get('Hackatime Project') as string, - pathway: record.get('Pathway') as string, - week: record.get('Week') as number, - screenshotUrl: (record.get('Screenshot') as Array<{ url: string }> | undefined)?.[0]?.url ?? null, - hoursSpent: (record.get('Optional - Override Hours Spent') as number | undefined) ?? null, - submittedAt: record._rawJson.createdTime as string - })); + const emails = Array.from( + new Set( + records + .map((r) => r.get('Email') as string | undefined) + .filter((e): e is string => typeof e === 'string' && e.length > 0) + ) + ); + + const slackIdByEmail = new Map(); + if (emails.length > 0) { + const users = await db + .select({ email: userTable.email, slackId: userTable.slackId }) + .from(userTable) + .where(inArray(userTable.email, emails)); + for (const u of users) { + slackIdByEmail.set(u.email, u.slackId); + } + } + + const submissions = records.map((record) => { + const email = record.get('Email') as string; + return { + id: record.id, + codeUrl: record.get('Code URL') as string, + playableUrl: record.get('Playable URL') as string, + description: record.get('Description') as string, + firstName: record.get('First Name') as string, + lastName: record.get('Last Name') as string, + email, + slackId: slackIdByEmail.get(email) ?? null, + githubUsername: record.get('GitHub Username') as string, + hackatimeProject: record.get('Hackatime Project') as string, + pathway: record.get('Pathway') as string, + week: record.get('Week') as number, + screenshotUrl: (record.get('Screenshot') as Array<{ url: string }> | undefined)?.[0]?.url ?? null, + hoursSpent: (record.get('Optional - Override Hours Spent') as number | undefined) ?? null, + submittedAt: record._rawJson.createdTime as string + }; + }); return json(submissions); } catch (err) { diff --git a/resolution-frontend/src/routes/app/reviewer/+page.svelte b/resolution-frontend/src/routes/app/reviewer/+page.svelte index 6b2c6c1..e908ae8 100644 --- a/resolution-frontend/src/routes/app/reviewer/+page.svelte +++ b/resolution-frontend/src/routes/app/reviewer/+page.svelte @@ -21,6 +21,7 @@ githubUsername: string; hoursSpent: number | null; submittedAt: string; + slackId: string | null; } const pathwayInfo = PATHWAY_INFO; @@ -210,6 +211,16 @@ {submission.hoursSpent}h reported {/if} + {#if submission.slackId != null} + + Slack ID +
{submission.slackId} + + {/if}

{truncate(submission.description, 150)}

@@ -464,6 +475,15 @@ gap: 0.25rem; } + .slack-label a { + color: inherit; + text-decoration: underline; + } + + .slack-label a:hover { + color: #338eda; + } + .description { font-size: 0.875rem; color: #1a1a2e; From 588080131bfcd33f76930949ee14337bf70b8486 Mon Sep 17 00:00:00 2001 From: Niko Yu <137782638+thesleepyniko@users.noreply.github.com> Date: Thu, 30 Apr 2026 12:44:48 -0400 Subject: [PATCH 17/20] fix: validation and normalization for some stuff --- .../src/routes/api/review/submissions/+server.ts | 9 +++++---- resolution-frontend/src/routes/app/reviewer/+page.svelte | 6 +++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/resolution-frontend/src/routes/api/review/submissions/+server.ts b/resolution-frontend/src/routes/api/review/submissions/+server.ts index 46cdccc..143515f 100644 --- a/resolution-frontend/src/routes/api/review/submissions/+server.ts +++ b/resolution-frontend/src/routes/api/review/submissions/+server.ts @@ -5,7 +5,7 @@ import type { RequestHandler } from './$types'; import { requireAuth } from '$lib/server/auth/guard'; import { db } from '$lib/server/db'; import { reviewerPathway, user as userTable } from '$lib/server/db/schema'; -import { eq, inArray } from 'drizzle-orm'; +import { eq, inArray, sql } from 'drizzle-orm'; import { PATHWAY_IDS } from '$lib/pathways'; export const GET: RequestHandler = async (event) => { @@ -76,6 +76,7 @@ export const GET: RequestHandler = async (event) => { records .map((r) => r.get('Email') as string | undefined) .filter((e): e is string => typeof e === 'string' && e.length > 0) + .map((e) => e.toLowerCase()) ) ); @@ -84,9 +85,9 @@ export const GET: RequestHandler = async (event) => { const users = await db .select({ email: userTable.email, slackId: userTable.slackId }) .from(userTable) - .where(inArray(userTable.email, emails)); + .where(inArray(sql`lower(${userTable.email})`, emails)); for (const u of users) { - slackIdByEmail.set(u.email, u.slackId); + slackIdByEmail.set(u.email.toLowerCase(), u.slackId); } } @@ -100,7 +101,7 @@ export const GET: RequestHandler = async (event) => { firstName: record.get('First Name') as string, lastName: record.get('Last Name') as string, email, - slackId: slackIdByEmail.get(email) ?? null, + slackId: slackIdByEmail.get(email?.toLowerCase()) ?? null, githubUsername: record.get('GitHub Username') as string, hackatimeProject: record.get('Hackatime Project') as string, pathway: record.get('Pathway') as string, diff --git a/resolution-frontend/src/routes/app/reviewer/+page.svelte b/resolution-frontend/src/routes/app/reviewer/+page.svelte index e908ae8..5e266ae 100644 --- a/resolution-frontend/src/routes/app/reviewer/+page.svelte +++ b/resolution-frontend/src/routes/app/reviewer/+page.svelte @@ -132,6 +132,10 @@ if (text.length <= max) return text; return text.slice(0, max) + '…'; } + + function isValidSlackId(id: string | null): id is string { + return typeof id === 'string' && /^[A-Z0-9]+$/.test(id); + } @@ -211,7 +215,7 @@ {submission.hoursSpent}h reported {/if} - {#if submission.slackId != null} + {#if isValidSlackId(submission.slackId)} Slack ID Date: Fri, 1 May 2026 13:06:12 -0400 Subject: [PATCH 18/20] fix: squash migrations into one --- .../drizzle/0005_productive_madripoor.sql | 2 +- .../drizzle/0006_happy_glorian.sql | 1 - .../drizzle/meta/0005_snapshot.json | 2 +- .../drizzle/meta/0006_snapshot.json | 1816 ----------------- .../drizzle/meta/_journal.json | 7 - 5 files changed, 2 insertions(+), 1826 deletions(-) delete mode 100644 resolution-frontend/drizzle/0006_happy_glorian.sql delete mode 100644 resolution-frontend/drizzle/meta/0006_snapshot.json diff --git a/resolution-frontend/drizzle/0005_productive_madripoor.sql b/resolution-frontend/drizzle/0005_productive_madripoor.sql index b7c5ff3..f017c3a 100644 --- a/resolution-frontend/drizzle/0005_productive_madripoor.sql +++ b/resolution-frontend/drizzle/0005_productive_madripoor.sql @@ -6,7 +6,7 @@ CREATE TABLE "submission_closure_exception" ( "week_number" integer NOT NULL, "reason" text NOT NULL, "is_active" boolean DEFAULT true NOT NULL, - "expires_at" timestamp NOT NULL, + "expires_at" date NOT NULL, "created_at" timestamp DEFAULT now() NOT NULL, "created_by" text NOT NULL ); diff --git a/resolution-frontend/drizzle/0006_happy_glorian.sql b/resolution-frontend/drizzle/0006_happy_glorian.sql deleted file mode 100644 index 2208666..0000000 --- a/resolution-frontend/drizzle/0006_happy_glorian.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE "submission_closure_exception" ALTER COLUMN "expires_at" SET DATA TYPE date; \ No newline at end of file diff --git a/resolution-frontend/drizzle/meta/0005_snapshot.json b/resolution-frontend/drizzle/meta/0005_snapshot.json index 53df726..098761f 100644 --- a/resolution-frontend/drizzle/meta/0005_snapshot.json +++ b/resolution-frontend/drizzle/meta/0005_snapshot.json @@ -955,7 +955,7 @@ }, "expires_at": { "name": "expires_at", - "type": "timestamp", + "type": "date", "primaryKey": false, "notNull": true }, diff --git a/resolution-frontend/drizzle/meta/0006_snapshot.json b/resolution-frontend/drizzle/meta/0006_snapshot.json deleted file mode 100644 index 2cabbd7..0000000 --- a/resolution-frontend/drizzle/meta/0006_snapshot.json +++ /dev/null @@ -1,1816 +0,0 @@ -{ - "id": "bd8506d4-a303-4b90-a2a3-175fe787468c", - "prevId": "097f7629-1e63-459c-803a-0dcbfbca8123", - "version": "7", - "dialect": "postgresql", - "tables": { - "public.ambassador_pathway": { - "name": "ambassador_pathway", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "pathway": { - "name": "pathway", - "type": "pathway", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "assigned_at": { - "name": "assigned_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "assigned_by": { - "name": "assigned_by", - "type": "text", - "primaryKey": false, - "notNull": true - } - }, - "indexes": { - "ambassador_pathway_unique_idx": { - "name": "ambassador_pathway_unique_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "pathway", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "ambassador_pathway_user_id_user_id_fk": { - "name": "ambassador_pathway_user_id_user_id_fk", - "tableFrom": "ambassador_pathway", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "ambassador_pathway_assigned_by_user_id_fk": { - "name": "ambassador_pathway_assigned_by_user_id_fk", - "tableFrom": "ambassador_pathway", - "tableTo": "user", - "columnsFrom": [ - "assigned_by" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.ambassador_payout": { - "name": "ambassador_payout", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "ambassador_id": { - "name": "ambassador_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "season_id": { - "name": "season_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "amount_cents": { - "name": "amount_cents", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "status": { - "name": "status", - "type": "payout_status", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'DRAFT'" - }, - "period_start": { - "name": "period_start", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "period_end": { - "name": "period_end", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "paid_at": { - "name": "paid_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "ambassador_payout_ambassador_id_user_id_fk": { - "name": "ambassador_payout_ambassador_id_user_id_fk", - "tableFrom": "ambassador_payout", - "tableTo": "user", - "columnsFrom": [ - "ambassador_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "ambassador_payout_season_id_program_season_id_fk": { - "name": "ambassador_payout_season_id_program_season_id_fk", - "tableFrom": "ambassador_payout", - "tableTo": "program_season", - "columnsFrom": [ - "season_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.ambassador_payout_item": { - "name": "ambassador_payout_item", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "payout_id": { - "name": "payout_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "workshop_id": { - "name": "workshop_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "completion_count": { - "name": "completion_count", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "rate_cents_per_completion": { - "name": "rate_cents_per_completion", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "amount_cents": { - "name": "amount_cents", - "type": "integer", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "ambassador_payout_item_payout_id_ambassador_payout_id_fk": { - "name": "ambassador_payout_item_payout_id_ambassador_payout_id_fk", - "tableFrom": "ambassador_payout_item", - "tableTo": "ambassador_payout", - "columnsFrom": [ - "payout_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "ambassador_payout_item_workshop_id_workshop_id_fk": { - "name": "ambassador_payout_item_workshop_id_workshop_id_fk", - "tableFrom": "ambassador_payout_item", - "tableTo": "workshop", - "columnsFrom": [ - "workshop_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.pathway_week_content": { - "name": "pathway_week_content", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "pathway": { - "name": "pathway", - "type": "pathway", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "week_number": { - "name": "week_number", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "title": { - "name": "title", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "''" - }, - "content": { - "name": "content", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "''" - }, - "prize_image_url": { - "name": "prize_image_url", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "is_published": { - "name": "is_published", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "is_submissions_open": { - "name": "is_submissions_open", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "last_edited_by": { - "name": "last_edited_by", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "pathway_week_content_unique_idx": { - "name": "pathway_week_content_unique_idx", - "columns": [ - { - "expression": "pathway", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "week_number", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "pathway_week_content_last_edited_by_user_id_fk": { - "name": "pathway_week_content_last_edited_by_user_id_fk", - "tableFrom": "pathway_week_content", - "tableTo": "user", - "columnsFrom": [ - "last_edited_by" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.program_enrollment": { - "name": "program_enrollment", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "season_id": { - "name": "season_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "role": { - "name": "role", - "type": "enrollment_role", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "status": { - "name": "status", - "type": "enrollment_status", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'ACTIVE'" - }, - "joined_at": { - "name": "joined_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "starting_week": { - "name": "starting_week", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 1 - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "enrollment_user_season_role_idx": { - "name": "enrollment_user_season_role_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "season_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "role", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "program_enrollment_user_id_user_id_fk": { - "name": "program_enrollment_user_id_user_id_fk", - "tableFrom": "program_enrollment", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "program_enrollment_season_id_program_season_id_fk": { - "name": "program_enrollment_season_id_program_season_id_fk", - "tableFrom": "program_enrollment", - "tableTo": "program_season", - "columnsFrom": [ - "season_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.program_season": { - "name": "program_season", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "slug": { - "name": "slug", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "signup_opens_at": { - "name": "signup_opens_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "signup_closes_at": { - "name": "signup_closes_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "starts_at": { - "name": "starts_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "ends_at": { - "name": "ends_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "total_weeks": { - "name": "total_weeks", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 8 - }, - "is_active": { - "name": "is_active", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "program_season_slug_unique": { - "name": "program_season_slug_unique", - "nullsNotDistinct": false, - "columns": [ - "slug" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.referral_link": { - "name": "referral_link", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "ambassador_id": { - "name": "ambassador_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "pathway": { - "name": "pathway", - "type": "pathway", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "code": { - "name": "code", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "label": { - "name": "label", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "is_active": { - "name": "is_active", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "referral_link_ambassador_id_user_id_fk": { - "name": "referral_link_ambassador_id_user_id_fk", - "tableFrom": "referral_link", - "tableTo": "user", - "columnsFrom": [ - "ambassador_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "referral_link_code_unique": { - "name": "referral_link_code_unique", - "nullsNotDistinct": false, - "columns": [ - "code" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.referral_signup": { - "name": "referral_signup", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "referral_link_id": { - "name": "referral_link_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "referral_signup_unique_idx": { - "name": "referral_signup_unique_idx", - "columns": [ - { - "expression": "referral_link_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "referral_signup_referral_link_id_referral_link_id_fk": { - "name": "referral_signup_referral_link_id_referral_link_id_fk", - "tableFrom": "referral_signup", - "tableTo": "referral_link", - "columnsFrom": [ - "referral_link_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "referral_signup_user_id_user_id_fk": { - "name": "referral_signup_user_id_user_id_fk", - "tableFrom": "referral_signup", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.reviewer_pathway": { - "name": "reviewer_pathway", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "pathway": { - "name": "pathway", - "type": "pathway", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "assigned_at": { - "name": "assigned_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "assigned_by": { - "name": "assigned_by", - "type": "text", - "primaryKey": false, - "notNull": true - } - }, - "indexes": { - "reviewer_pathway_unique_idx": { - "name": "reviewer_pathway_unique_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "pathway", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "reviewer_pathway_user_id_user_id_fk": { - "name": "reviewer_pathway_user_id_user_id_fk", - "tableFrom": "reviewer_pathway", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "reviewer_pathway_assigned_by_user_id_fk": { - "name": "reviewer_pathway_assigned_by_user_id_fk", - "tableFrom": "reviewer_pathway", - "tableTo": "user", - "columnsFrom": [ - "assigned_by" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.session": { - "name": "session", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "session_user_id_user_id_fk": { - "name": "session_user_id_user_id_fk", - "tableFrom": "session", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.submission_closure_exception": { - "name": "submission_closure_exception", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "season_id": { - "name": "season_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "pathway": { - "name": "pathway", - "type": "pathway", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "week_number": { - "name": "week_number", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "reason": { - "name": "reason", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "is_active": { - "name": "is_active", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "expires_at": { - "name": "expires_at", - "type": "date", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "created_by": { - "name": "created_by", - "type": "text", - "primaryKey": false, - "notNull": true - } - }, - "indexes": { - "submission_exception_unique_idx": { - "name": "submission_exception_unique_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "season_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "pathway", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "week_number", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "submission_exception_lookup_idx": { - "name": "submission_exception_lookup_idx", - "columns": [ - { - "expression": "season_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "pathway", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "week_number", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "submission_exception_user_idx": { - "name": "submission_exception_user_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "season_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "submission_closure_exception_user_id_user_id_fk": { - "name": "submission_closure_exception_user_id_user_id_fk", - "tableFrom": "submission_closure_exception", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "submission_closure_exception_season_id_program_season_id_fk": { - "name": "submission_closure_exception_season_id_program_season_id_fk", - "tableFrom": "submission_closure_exception", - "tableTo": "program_season", - "columnsFrom": [ - "season_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "submission_closure_exception_created_by_user_id_fk": { - "name": "submission_closure_exception_created_by_user_id_fk", - "tableFrom": "submission_closure_exception", - "tableTo": "user", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.user": { - "name": "user", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "hack_club_id": { - "name": "hack_club_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "first_name": { - "name": "first_name", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "last_name": { - "name": "last_name", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "slack_id": { - "name": "slack_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "verification_status": { - "name": "verification_status", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "ysws_eligible": { - "name": "ysws_eligible", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "is_admin": { - "name": "is_admin", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "user_email_unique": { - "name": "user_email_unique", - "nullsNotDistinct": false, - "columns": [ - "email" - ] - }, - "user_hack_club_id_unique": { - "name": "user_hack_club_id_unique", - "nullsNotDistinct": false, - "columns": [ - "hack_club_id" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.user_pathway": { - "name": "user_pathway", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "pathway": { - "name": "pathway", - "type": "pathway", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "user_pathway_unique_idx": { - "name": "user_pathway_unique_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "pathway", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "user_pathway_user_id_user_id_fk": { - "name": "user_pathway_user_id_user_id_fk", - "tableFrom": "user_pathway", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.weekly_ship": { - "name": "weekly_ship", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "season_id": { - "name": "season_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "workshop_id": { - "name": "workshop_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "week_number": { - "name": "week_number", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "goal_text": { - "name": "goal_text", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "status": { - "name": "status", - "type": "ship_status", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'PLANNED'" - }, - "proof_url": { - "name": "proof_url", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "notes": { - "name": "notes", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "shipped_at": { - "name": "shipped_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "ship_user_season_week_idx": { - "name": "ship_user_season_week_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "season_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "week_number", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "weekly_ship_user_id_user_id_fk": { - "name": "weekly_ship_user_id_user_id_fk", - "tableFrom": "weekly_ship", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "weekly_ship_season_id_program_season_id_fk": { - "name": "weekly_ship_season_id_program_season_id_fk", - "tableFrom": "weekly_ship", - "tableTo": "program_season", - "columnsFrom": [ - "season_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "weekly_ship_workshop_id_workshop_id_fk": { - "name": "weekly_ship_workshop_id_workshop_id_fk", - "tableFrom": "weekly_ship", - "tableTo": "workshop", - "columnsFrom": [ - "workshop_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.workshop": { - "name": "workshop", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "author_id": { - "name": "author_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "season_id": { - "name": "season_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "title": { - "name": "title", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "pathway": { - "name": "pathway", - "type": "pathway", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "difficulty": { - "name": "difficulty", - "type": "difficulty", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "estimated_hours": { - "name": "estimated_hours", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "published": { - "name": "published", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "workshop_author_id_user_id_fk": { - "name": "workshop_author_id_user_id_fk", - "tableFrom": "workshop", - "tableTo": "user", - "columnsFrom": [ - "author_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "workshop_season_id_program_season_id_fk": { - "name": "workshop_season_id_program_season_id_fk", - "tableFrom": "workshop", - "tableTo": "program_season", - "columnsFrom": [ - "season_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.workshop_analytics": { - "name": "workshop_analytics", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workshop_id": { - "name": "workshop_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "views": { - "name": "views", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "starts": { - "name": "starts", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "completions": { - "name": "completions", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "avg_completion_mins": { - "name": "avg_completion_mins", - "type": "real", - "primaryKey": false, - "notNull": false - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "workshop_analytics_workshop_id_workshop_id_fk": { - "name": "workshop_analytics_workshop_id_workshop_id_fk", - "tableFrom": "workshop_analytics", - "tableTo": "workshop", - "columnsFrom": [ - "workshop_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "workshop_analytics_workshop_id_unique": { - "name": "workshop_analytics_workshop_id_unique", - "nullsNotDistinct": false, - "columns": [ - "workshop_id" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.workshop_completion": { - "name": "workshop_completion", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "workshop_id": { - "name": "workshop_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "participant_id": { - "name": "participant_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "season_id": { - "name": "season_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "project_url": { - "name": "project_url", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "started_at": { - "name": "started_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "completed_at": { - "name": "completed_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "completion_workshop_participant_season_idx": { - "name": "completion_workshop_participant_season_idx", - "columns": [ - { - "expression": "workshop_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "participant_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "season_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "workshop_completion_workshop_id_workshop_id_fk": { - "name": "workshop_completion_workshop_id_workshop_id_fk", - "tableFrom": "workshop_completion", - "tableTo": "workshop", - "columnsFrom": [ - "workshop_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "workshop_completion_participant_id_user_id_fk": { - "name": "workshop_completion_participant_id_user_id_fk", - "tableFrom": "workshop_completion", - "tableTo": "user", - "columnsFrom": [ - "participant_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "workshop_completion_season_id_program_season_id_fk": { - "name": "workshop_completion_season_id_program_season_id_fk", - "tableFrom": "workshop_completion", - "tableTo": "program_season", - "columnsFrom": [ - "season_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - } - }, - "enums": { - "public.difficulty": { - "name": "difficulty", - "schema": "public", - "values": [ - "BEGINNER", - "INTERMEDIATE", - "ADVANCED" - ] - }, - "public.enrollment_role": { - "name": "enrollment_role", - "schema": "public", - "values": [ - "PARTICIPANT", - "AMBASSADOR" - ] - }, - "public.enrollment_status": { - "name": "enrollment_status", - "schema": "public", - "values": [ - "ACTIVE", - "DROPPED", - "COMPLETED" - ] - }, - "public.pathway": { - "name": "pathway", - "schema": "public", - "values": [ - "PYTHON", - "RUST", - "GAME_DEV", - "HARDWARE", - "DESIGN", - "GENERAL_CODING" - ] - }, - "public.payout_status": { - "name": "payout_status", - "schema": "public", - "values": [ - "DRAFT", - "PENDING", - "PAID", - "CANCELED" - ] - }, - "public.ship_status": { - "name": "ship_status", - "schema": "public", - "values": [ - "PLANNED", - "IN_PROGRESS", - "SHIPPED", - "MISSED" - ] - } - }, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} \ No newline at end of file diff --git a/resolution-frontend/drizzle/meta/_journal.json b/resolution-frontend/drizzle/meta/_journal.json index 7a5edef..3412586 100644 --- a/resolution-frontend/drizzle/meta/_journal.json +++ b/resolution-frontend/drizzle/meta/_journal.json @@ -43,13 +43,6 @@ "when": 1776740262163, "tag": "0005_productive_madripoor", "breakpoints": true - }, - { - "idx": 6, - "version": "7", - "when": 1776742511832, - "tag": "0006_happy_glorian", - "breakpoints": true } ] } \ No newline at end of file From 3c0854adb0be9bbf208038bb18e12b313b2086d1 Mon Sep 17 00:00:00 2001 From: Niko Yu <137782638+thesleepyniko@users.noreply.github.com> Date: Fri, 1 May 2026 13:06:34 -0400 Subject: [PATCH 19/20] feat: fix wrap-around + replace name --- .../src/routes/app/reviewer/+page.svelte | 41 +++++++++++-------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/resolution-frontend/src/routes/app/reviewer/+page.svelte b/resolution-frontend/src/routes/app/reviewer/+page.svelte index 5e266ae..76c4299 100644 --- a/resolution-frontend/src/routes/app/reviewer/+page.svelte +++ b/resolution-frontend/src/routes/app/reviewer/+page.svelte @@ -188,7 +188,9 @@ {@const info = pathwayInfo[submission.pathway]}
@@ -441,6 +437,8 @@ display: flex; flex-direction: column; gap: 0.75rem; + min-width: 0; + overflow: hidden; } .card-header { @@ -450,9 +448,13 @@ gap: 0.5rem; } - .submitter-name { + .project-name { font-weight: 600; font-size: 1rem; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .pathway-badge { @@ -468,20 +470,31 @@ .card-meta { display: flex; - gap: 1rem; + flex-wrap: wrap; + gap: 0.5rem 1rem; font-size: 0.8rem; color: #8492a6; + min-width: 0; } .card-meta span { - display: flex; + display: inline-flex; align-items: center; gap: 0.25rem; + min-width: 0; + max-width: 100%; + } + + .slack-label { + position: relative; + z-index: 1; } .slack-label a { color: inherit; text-decoration: underline; + word-break: break-all; + overflow-wrap: anywhere; } .slack-label a:hover { @@ -528,14 +541,6 @@ background: rgba(255, 255, 255, 1); } - .hackatime-label { - display: inline-flex; - align-items: center; - gap: 0.375rem; - font-size: 0.8rem; - color: #8492a6; - } - .card-actions { display: flex; gap: 0.5rem; From d6153dafe639c0684421a9bf5bafa2c4ceaad443 Mon Sep 17 00:00:00 2001 From: Niko Yu <137782638+thesleepyniko@users.noreply.github.com> Date: Fri, 1 May 2026 13:06:57 -0400 Subject: [PATCH 20/20] fix: exception comments in PR #82 --- .../server/services/exceptionService.test.ts | 77 +++++++- .../app/ambassador/exceptions/+page.server.ts | 168 +++++++++++++----- .../app/ambassador/exceptions/+page.svelte | 81 ++++++++- .../[pathway]/week/[week]/+page.server.ts | 3 +- .../[pathway]/week/[week]/+page.svelte | 2 +- 5 files changed, 279 insertions(+), 52 deletions(-) diff --git a/resolution-frontend/src/lib/server/services/exceptionService.test.ts b/resolution-frontend/src/lib/server/services/exceptionService.test.ts index 16035c0..f3c6769 100644 --- a/resolution-frontend/src/lib/server/services/exceptionService.test.ts +++ b/resolution-frontend/src/lib/server/services/exceptionService.test.ts @@ -13,17 +13,64 @@ vi.mock('../db', () => ({ }, select: (...args: unknown[]) => { mockSelect(...args); - return { from: (...a: unknown[]) => { mockFrom(...a); return { where: (...w: unknown[]) => { mockWhere(...w); return { limit: (...l: unknown[]) => mockLimit(...l) }; } }; } }; + return { + from: (...a: unknown[]) => { + mockFrom(...a); + return { + where: (...w: unknown[]) => { + mockWhere(...w); + return { limit: (...l: unknown[]) => mockLimit(...l) }; + } + }; + } + }; } } })); const { ExceptionService } = await import('./exceptionService'); +const { submissionClosureException } = await import('../db/schema'); beforeEach(() => { vi.clearAllMocks(); }); +/** + * Walk a drizzle SQL/condition node and collect the columns referenced by every + * `eq(column, ...)`/`gte(column, ...)` operator. We assert against this so that + * if anyone removes one of the filters from `getActiveException`, the test fails. + */ +function collectReferencedColumns( + node: unknown, + out: Set = new Set(), + seen: WeakSet = new WeakSet() +): Set { + if (!node || typeof node !== 'object') return out; + if (seen.has(node as object)) return out; + seen.add(node as object); + const n = node as Record; + + // drizzle Column instances expose `.name` plus column-ish metadata. + if (typeof n.name === 'string' && (n.columnType || n.dataType)) { + out.add(n.name as string); + // don't recurse into a column's `.table` back-reference; that's the + // full table definition and would re-walk every other column. + return out; + } + + for (const key of Object.keys(n)) { + // avoid drizzle internal back-references that explode the walk. + if (key === 'table' || key === 'schema') continue; + const value = n[key]; + if (Array.isArray(value)) { + for (const item of value) collectReferencedColumns(item, out, seen); + } else if (value && typeof value === 'object') { + collectReferencedColumns(value, out, seen); + } + } + return out; +} + describe('ExceptionService.getActiveException', () => { it('returns null when no active season exists', async () => { mockFindFirstSeason.mockResolvedValue(undefined); @@ -50,7 +97,7 @@ describe('ExceptionService.getActiveException', () => { expect(result).toEqual(exception); }); - it('queries with correct season id', async () => { + it('queries with limit 1', async () => { mockFindFirstSeason.mockResolvedValue({ id: 'season-42', isActive: true }); mockLimit.mockResolvedValue([]); @@ -61,6 +108,32 @@ describe('ExceptionService.getActiveException', () => { expect(mockLimit).toHaveBeenCalledWith(1); }); + it('filters on userId, seasonId, pathway, weekNumber, isActive, and expiresAt', async () => { + mockFindFirstSeason.mockResolvedValue({ id: 'season-1', isActive: true }); + mockLimit.mockResolvedValue([]); + + await ExceptionService.getActiveException('user-1', 'PYTHON', 1); + + expect(mockWhere).toHaveBeenCalledTimes(1); + const whereArg = mockWhere.mock.calls[0][0]; + const referenced = collectReferencedColumns(whereArg); + + // Each of these filters is critical to the security/correctness of the + // service. Removing any of them would silently allow incorrect bypasses. + const expectedColumns = [ + submissionClosureException.userId.name, + submissionClosureException.seasonId.name, + submissionClosureException.pathway.name, + submissionClosureException.weekNumber.name, + submissionClosureException.isActive.name, + submissionClosureException.expiresAt.name + ]; + + for (const col of expectedColumns) { + expect(referenced.has(col), `expected where clause to reference column "${col}"`).toBe(true); + } + }); + it('returns null for empty array result', async () => { mockFindFirstSeason.mockResolvedValue({ id: 'season-1', isActive: true }); mockLimit.mockResolvedValue([]); diff --git a/resolution-frontend/src/routes/app/ambassador/exceptions/+page.server.ts b/resolution-frontend/src/routes/app/ambassador/exceptions/+page.server.ts index e5c6a23..224fe48 100644 --- a/resolution-frontend/src/routes/app/ambassador/exceptions/+page.server.ts +++ b/resolution-frontend/src/routes/app/ambassador/exceptions/+page.server.ts @@ -1,9 +1,23 @@ import type { PageServerLoad, Actions } from './$types'; import { db } from '$lib/server/db'; -import { ambassadorPathway, submissionClosureException, programEnrollment, programSeason, user } from '$lib/server/db/schema'; +import { + ambassadorPathway, + submissionClosureException, + programEnrollment, + programSeason, + user, + userPathway +} from '$lib/server/db/schema'; import { eq, and, inArray } from 'drizzle-orm'; import { error, fail } from '@sveltejs/kit'; -import { PATHWAY_IDS } from '$lib/pathways'; +import { PATHWAY_IDS, type PathwayId } from '$lib/pathways'; + +function isPathwayId(value: string): value is PathwayId { + return (PATHWAY_IDS as readonly string[]).includes(value); +} + +// Postgres unique violation +const PG_UNIQUE_VIOLATION = '23505'; export const load: PageServerLoad = async ({ parent }) => { const { user: currentUser } = await parent(); @@ -17,7 +31,11 @@ export const load: PageServerLoad = async ({ parent }) => { throw error(403, 'You are not an ambassador'); } - const assignedPathways = assignments.map((a) => a.pathway); + // Admins can manage exceptions for any pathway, even without explicit + // ambassador assignments. + const assignedPathways: PathwayId[] = currentUser.isAdmin + ? ([...PATHWAY_IDS] as PathwayId[]) + : assignments.map((a) => a.pathway as PathwayId); const season = await db.query.programSeason.findFirst({ where: eq(programSeason.isActive, true) @@ -27,22 +45,41 @@ export const load: PageServerLoad = async ({ parent }) => { throw error(500, 'No active season configured'); } + const enrolledUsersBaseWhere = and( + eq(programEnrollment.seasonId, season.id), + eq(programEnrollment.status, 'ACTIVE') + ); + + const enrolledUsersQuery = currentUser.isAdmin + ? db + .select({ + id: user.id, + firstName: user.firstName, + lastName: user.lastName, + email: user.email + }) + .from(programEnrollment) + .innerJoin(user, eq(programEnrollment.userId, user.id)) + .where(enrolledUsersBaseWhere) + : db + .selectDistinct({ + id: user.id, + firstName: user.firstName, + lastName: user.lastName, + email: user.email + }) + .from(programEnrollment) + .innerJoin(user, eq(programEnrollment.userId, user.id)) + .innerJoin(userPathway, eq(userPathway.userId, user.id)) + .where( + and( + enrolledUsersBaseWhere, + inArray(userPathway.pathway, assignedPathways) + ) + ); + const [enrolledUsers, exceptions] = await Promise.all([ - db - .select({ - id: user.id, - firstName: user.firstName, - lastName: user.lastName, - email: user.email - }) - .from(programEnrollment) - .innerJoin(user, eq(programEnrollment.userId, user.id)) - .where( - and( - eq(programEnrollment.seasonId, season.id), - eq(programEnrollment.status, 'ACTIVE') - ) - ), + enrolledUsersQuery, db .select({ @@ -54,6 +91,7 @@ export const load: PageServerLoad = async ({ parent }) => { isActive: submissionClosureException.isActive, expiresAt: submissionClosureException.expiresAt, createdAt: submissionClosureException.createdAt, + createdBy: submissionClosureException.createdBy, userName: user.firstName, userLastName: user.lastName, userEmail: user.email @@ -61,13 +99,13 @@ export const load: PageServerLoad = async ({ parent }) => { .from(submissionClosureException) .innerJoin(user, eq(submissionClosureException.userId, user.id)) .where( - currentUser.isAdmin - ? eq(submissionClosureException.seasonId, season.id) - : and( - eq(submissionClosureException.seasonId, season.id), - inArray(submissionClosureException.pathway, assignedPathways) - ) - ) + currentUser.isAdmin + ? eq(submissionClosureException.seasonId, season.id) + : and( + eq(submissionClosureException.seasonId, season.id), + inArray(submissionClosureException.pathway, assignedPathways) + ) + ) ]); return { @@ -91,18 +129,33 @@ export const actions: Actions = { const formData = await request.formData(); const userId = formData.get('userId') as string; const pathway = formData.get('pathway') as string; - const weekNumber = parseInt(formData.get('weekNumber') as string, 10); + const weekNumberRaw = formData.get('weekNumber') as string; + const weekNumber = parseInt(weekNumberRaw, 10); const reason = formData.get('reason') as string; - const expiresAt = formData.get('expiresAt') as string; + const expiresAtRaw = formData.get('expiresAt') as string; - if (!userId || !pathway || !weekNumber || !reason || !expiresAt) { + if (!userId || !pathway || !weekNumberRaw || Number.isNaN(weekNumber) || !reason || !expiresAtRaw) { return fail(400, { error: 'All fields are required' }); } - if (!PATHWAY_IDS.includes(pathway)) { + if (!isPathwayId(pathway)) { return fail(400, { error: 'Invalid pathway' }); } + // Validate expiresAt is a real YYYY-MM-DD date and not in the past. + if (!/^\d{4}-\d{2}-\d{2}$/.test(expiresAtRaw)) { + return fail(400, { error: 'Invalid expiration date format' }); + } + const parsedExpires = new Date(expiresAtRaw + 'T00:00:00'); + if (Number.isNaN(parsedExpires.getTime())) { + return fail(400, { error: 'Invalid expiration date' }); + } + const today = new Date(); + const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`; + if (expiresAtRaw < todayStr) { + return fail(400, { error: 'Expiration date must be today or in the future' }); + } + const season = await db.query.programSeason.findFirst({ where: eq(programSeason.isActive, true) }); @@ -115,31 +168,55 @@ export const actions: Actions = { return fail(400, { error: 'Invalid week number' }); } - const assignment = await db.query.ambassadorPathway.findFirst({ - where: and( - eq(ambassadorPathway.userId, locals.user.id), - eq(ambassadorPathway.pathway, pathway as any) - ) + // Verify the current ambassador is assigned to this pathway (admins bypass). + if (!locals.user.isAdmin) { + const assignment = await db.query.ambassadorPathway.findFirst({ + where: and( + eq(ambassadorPathway.userId, locals.user.id), + eq(ambassadorPathway.pathway, pathway) + ) + }); + + if (!assignment) { + return fail(403, { error: 'You are not assigned to this pathway' }); + } + } + + // Verify the target user is enrolled in this pathway. + const targetEnrollment = await db.query.userPathway.findFirst({ + where: and(eq(userPathway.userId, userId), eq(userPathway.pathway, pathway)) }); - if (!assignment && !locals.user.isAdmin) { - return fail(403, { error: 'You are not assigned to this pathway' }); + if (!targetEnrollment) { + return fail(400, { error: 'Selected user is not enrolled in this pathway' }); } try { await db.insert(submissionClosureException).values({ userId, seasonId: season.id, - pathway: pathway as any, + pathway, weekNumber, reason, - expiresAt, + expiresAt: expiresAtRaw, createdBy: locals.user.id }); - } catch { - return fail(400, { error: 'An exception already exists for this user, pathway, and week' }); + } catch (err) { + const code = (err as { code?: string } | null)?.code; + if (code === PG_UNIQUE_VIOLATION) { + return fail(400, { + error: 'An exception already exists for this user, pathway, and week' + }); + } + console.error('[exceptions] Failed to insert exception', err); + return fail(500, { error: 'Failed to create exception' }); } + // Audit log: who granted this submission bypass. + console.info( + `[exceptions] createException by user=${locals.user.id} for target=${userId} pathway=${pathway} week=${weekNumber} expiresAt=${expiresAtRaw}` + ); + return { success: true }; }, @@ -175,11 +252,16 @@ export const actions: Actions = { } } + const newState = !exception.isActive; await db .update(submissionClosureException) - .set({ isActive: !exception.isActive }) + .set({ isActive: newState }) .where(eq(submissionClosureException.id, exceptionId)); + console.info( + `[exceptions] toggleException by user=${locals.user.id} exception=${exceptionId} isActive=${newState}` + ); + return { success: true }; }, @@ -217,6 +299,10 @@ export const actions: Actions = { await db.delete(submissionClosureException).where(eq(submissionClosureException.id, exceptionId)); + console.info( + `[exceptions] deleteException by user=${locals.user.id} exception=${exceptionId}` + ); + return { success: true }; } }; diff --git a/resolution-frontend/src/routes/app/ambassador/exceptions/+page.svelte b/resolution-frontend/src/routes/app/ambassador/exceptions/+page.svelte index a2bbd45..49b30d5 100644 --- a/resolution-frontend/src/routes/app/ambassador/exceptions/+page.svelte +++ b/resolution-frontend/src/routes/app/ambassador/exceptions/+page.svelte @@ -39,16 +39,36 @@ } function formatDate(date: Date | string) { - return new Date(date).toLocaleDateString('en-US', { + // expiresAt is a date-only string (YYYY-MM-DD); appending T00:00:00 + // keeps it in local time so it doesn't display the previous day in + // negative-UTC-offset zones. + const d = typeof date === 'string' ? new Date(date + 'T00:00:00') : date; + return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); } + function todayStr() { + const t = new Date(); + return `${t.getFullYear()}-${String(t.getMonth() + 1).padStart(2, '0')}-${String(t.getDate()).padStart(2, '0')}`; + } + + function isExpired(expiresAt: string | Date) { + const s = typeof expiresAt === 'string' ? expiresAt : expiresAt.toISOString().slice(0, 10); + return s < todayStr(); + } + const canSubmit = $derived( selectedUserId && selectedPathway && selectedWeek && reason && expiresAt ); + + const selectedUserDisplay = $derived( + selectedUserId + ? data.enrolledUsers.find((u) => u.id === selectedUserId) + : null + ); @@ -144,6 +164,17 @@ {/if} {/if} + {#if selectedUserDisplay} +
+ Selected: + + {[selectedUserDisplay.firstName, selectedUserDisplay.lastName] + .filter(Boolean) + .join(' ') || selectedUserDisplay.email} + + ({selectedUserDisplay.email}) +
+ {/if}
@@ -215,17 +246,24 @@
{:else}
-

Active Exceptions ({data.exceptions.length})

+

Exceptions ({data.exceptions.length})

{#each data.exceptions as exception} {@const info = pathwayInfo[exception.pathway]} -
+ {@const expired = isExpired(exception.expiresAt)} + {@const currentlyActive = exception.isActive && !expired} +
- {[exception.userName, exception.userLastName].filter(Boolean).join(' ')} - + {[exception.userName, exception.userLastName] + .filter(Boolean) + .join(' ')} {exception.userEmail}
@@ -234,8 +272,18 @@ {info.label} Week {exception.weekNumber} - - {exception.isActive ? 'Active' : 'Inactive'} + + {#if !exception.isActive} + Inactive + {:else if expired} + Expired + {:else} + Active + {/if} Expires {formatDate(exception.expiresAt)} @@ -624,6 +672,25 @@ color: #33d6a6; } + .status-badge.expired-badge { + background: #fff3e0; + color: #ff8c37; + } + + .selected-user { + margin-top: 0.375rem; + font-size: 0.8rem; + color: #1a1a2e; + } + + .selected-user-email { + color: #8492a6; + } + + .exception-item.expired { + opacity: 0.85; + } + .exception-reason { font-size: 0.85rem; color: #1a1a2e; diff --git a/resolution-frontend/src/routes/app/pathway/[pathway]/week/[week]/+page.server.ts b/resolution-frontend/src/routes/app/pathway/[pathway]/week/[week]/+page.server.ts index 8e0dd38..8c280ad 100644 --- a/resolution-frontend/src/routes/app/pathway/[pathway]/week/[week]/+page.server.ts +++ b/resolution-frontend/src/routes/app/pathway/[pathway]/week/[week]/+page.server.ts @@ -52,7 +52,8 @@ export const load: PageServerLoad = async ({ params, parent }) => { weekNumber, title: content.title, content: content.content, - isSubmissionsOpen: content.isSubmissionsOpen || !!exception, + isSubmissionsOpen: content.isSubmissionsOpen, + hasException: !!exception, exception }; }; diff --git a/resolution-frontend/src/routes/app/pathway/[pathway]/week/[week]/+page.svelte b/resolution-frontend/src/routes/app/pathway/[pathway]/week/[week]/+page.svelte index f20e589..132216e 100644 --- a/resolution-frontend/src/routes/app/pathway/[pathway]/week/[week]/+page.svelte +++ b/resolution-frontend/src/routes/app/pathway/[pathway]/week/[week]/+page.svelte @@ -16,7 +16,7 @@

Ready to ship?

- {#if data.isSubmissionsOpen} + {#if data.isSubmissionsOpen || data.hasException} {#if data.exception}