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 @@
+
+
+ Create and manage referral links for your pathways You haven't created any referral links yet. Use the form above to create your first link.
+ Back to Ambassador Dashboard
+
+
+
Referral Links
+ Create Referral Link
+
+
+
{info.label}
+ {links.length} link{links.length !== 1 ? 's' : ''}
+ {$page.url.origin}/ref/{link.code}
+
+
Manage your pathway content
- -Create and manage referral links for your pathways
+Grant deadline extensions for {data.season.name}
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.
{$page.url.origin}/ref/{link.code}
-
- 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 @@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}{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); + }