diff --git a/.github/workflows/assign_random_reviewer.yaml b/.github/workflows/assign_random_reviewer.yaml
deleted file mode 100644
index d28682a1c..000000000
--- a/.github/workflows/assign_random_reviewer.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-name: "Assign Random Reviewer"
-on:
- pull_request_target:
- types: [opened, ready_for_review, reopened]
-
-jobs:
- assign_reviewer:
- runs-on: ubuntu-latest
- if: github.event.pull_request.draft == false
- steps:
- - uses: actions/checkout@v3
- - uses: uesteibar/reviewer-lottery@v2
- with:
- repo-token: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
diff --git a/.github/workflows/deploy_production.yml b/.github/workflows/deploy_production.yml
index 9be62d43a..77bbd15dc 100644
--- a/.github/workflows/deploy_production.yml
+++ b/.github/workflows/deploy_production.yml
@@ -45,6 +45,7 @@ env:
HOSTED_ZONE_ID: ${{ secrets.HOSTED_ZONE_ID }}
CERTIFICATE_ARN: ${{ secrets.CERTIFICATE_ARN }}
PR_NUM: ${{ github.event.pull_request.number }}
+ STAGE: prod
jobs:
# Build production version of the frontend and upload the artifacts.
diff --git a/.github/workflows/deploy_staging.yml b/.github/workflows/deploy_staging.yml
index b7940602d..c6f665ff5 100644
--- a/.github/workflows/deploy_staging.yml
+++ b/.github/workflows/deploy_staging.yml
@@ -45,6 +45,7 @@ env:
HOSTED_ZONE_ID: ${{ secrets.HOSTED_ZONE_ID }}
CERTIFICATE_ARN: ${{ secrets.CERTIFICATE_ARN }}
PR_NUM: ${{ github.event.pull_request.number }}
+ STAGE: dev
jobs:
get_staging_configuration:
diff --git a/.github/workflows/destroy_staging.yml b/.github/workflows/destroy_staging.yml
index 1a8c26a85..31e201ede 100644
--- a/.github/workflows/destroy_staging.yml
+++ b/.github/workflows/destroy_staging.yml
@@ -42,6 +42,7 @@ env:
HOSTED_ZONE_ID: ${{ secrets.HOSTED_ZONE_ID }}
CERTIFICATE_ARN: ${{ secrets.CERTIFICATE_ARN }}
PR_NUM: ${{ github.event.pull_request.number }}
+ STAGE: dev
jobs:
destroy_staging_frontend:
diff --git a/apps/antalmanac/src/actions/ActionTypesStore.ts b/apps/antalmanac/src/actions/ActionTypesStore.ts
index c57d018a3..0284234ef 100644
--- a/apps/antalmanac/src/actions/ActionTypesStore.ts
+++ b/apps/antalmanac/src/actions/ActionTypesStore.ts
@@ -59,7 +59,7 @@ export interface ClearScheduleAction {
export interface CopyScheduleAction {
type: 'copySchedule';
- to: number;
+ newScheduleName: string;
}
export interface ChangeCourseColorAction {
@@ -159,7 +159,7 @@ class ActionTypesStore extends EventEmitter {
AppStore.schedule.clearCurrentSchedule();
break;
case 'copySchedule':
- AppStore.schedule.copySchedule(action.to);
+ AppStore.schedule.copySchedule(action.newScheduleName);
break;
default:
break;
diff --git a/apps/antalmanac/src/actions/AppStoreActions.ts b/apps/antalmanac/src/actions/AppStoreActions.ts
index 78bbf2c55..e54a5be49 100644
--- a/apps/antalmanac/src/actions/AppStoreActions.ts
+++ b/apps/antalmanac/src/actions/AppStoreActions.ts
@@ -11,8 +11,8 @@ import { removeLocalStorageUserId, setLocalStorageUserId } from '$lib/localStora
import AppStore from '$stores/AppStore';
export interface CopyScheduleOptions {
- onSuccess: (index: number) => unknown;
- onError: (index: number) => unknown;
+ onSuccess: (scheduleName: string) => unknown;
+ onError: (scheduleName: string) => unknown;
}
export const addCourse = (
@@ -250,17 +250,17 @@ export const changeCourseColor = (sectionCode: string, term: string, newColor: s
AppStore.changeCourseColor(sectionCode, term, newColor);
};
-export const copySchedule = (to: number, options?: CopyScheduleOptions) => {
+export const copySchedule = (newScheduleName: string, options?: CopyScheduleOptions) => {
logAnalytics({
category: analyticsEnum.addedClasses.title,
action: analyticsEnum.addedClasses.actions.COPY_SCHEDULE,
});
try {
- AppStore.copySchedule(to);
- options?.onSuccess(to);
+ AppStore.copySchedule(newScheduleName);
+ options?.onSuccess(newScheduleName);
} catch (error) {
- options?.onError(to);
+ options?.onError(newScheduleName);
}
};
diff --git a/apps/antalmanac/src/components/dialogs/AddSchedule.tsx b/apps/antalmanac/src/components/dialogs/AddSchedule.tsx
index 3bf91f87a..ae4bc6305 100644
--- a/apps/antalmanac/src/components/dialogs/AddSchedule.tsx
+++ b/apps/antalmanac/src/components/dialogs/AddSchedule.tsx
@@ -1,6 +1,6 @@
import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, TextField } from '@mui/material';
import type { DialogProps } from '@mui/material';
-import { useState } from 'react';
+import { useState, useEffect, useCallback } from 'react';
import { addSchedule } from '$actions/AppStoreActions';
import AppStore from '$stores/AppStore';
@@ -12,7 +12,9 @@ import { useThemeStore } from '$stores/SettingsStore';
function AddScheduleDialog({ onClose, onKeyDown, ...props }: DialogProps) {
const isDark = useThemeStore((store) => store.isDark);
- const [name, setName] = useState(AppStore.getDefaultScheduleName());
+ const [name, setName] = useState(
+ AppStore.getNextScheduleName(AppStore.getDefaultScheduleName(), AppStore.getScheduleNames().length)
+ );
const handleCancel = () => {
onClose?.({}, 'escapeKeyDown');
@@ -24,7 +26,6 @@ function AddScheduleDialog({ onClose, onKeyDown, ...props }: DialogProps) {
const submitName = () => {
addSchedule(name);
- setName(AppStore.schedule.getDefaultScheduleName());
onClose?.({}, 'escapeKeyDown');
};
@@ -46,6 +47,17 @@ function AddScheduleDialog({ onClose, onKeyDown, ...props }: DialogProps) {
}
};
+ const handleScheduleNamesChange = useCallback(() => {
+ setName(AppStore.getNextScheduleName(AppStore.getDefaultScheduleName(), AppStore.getScheduleNames().length));
+ }, []);
+
+ useEffect(() => {
+ AppStore.on('scheduleNamesChange', handleScheduleNamesChange);
+ return () => {
+ AppStore.off('scheduleNamesChange', handleScheduleNamesChange);
+ };
+ }, [handleScheduleNamesChange]);
+
return (
-
-
+
);
};
diff --git a/apps/antalmanac/src/stores/AppStore.ts b/apps/antalmanac/src/stores/AppStore.ts
index bfcd43456..b9ac5c496 100644
--- a/apps/antalmanac/src/stores/AppStore.ts
+++ b/apps/antalmanac/src/stores/AppStore.ts
@@ -71,6 +71,10 @@ class AppStore extends EventEmitter {
}
}
+ getNextScheduleName(newScheduleName: string, scheduleIndex: number) {
+ return this.schedule.getNextScheduleName(newScheduleName, scheduleIndex);
+ }
+
getDefaultScheduleName() {
return this.schedule.getDefaultScheduleName();
}
@@ -291,14 +295,17 @@ class AppStore extends EventEmitter {
window.localStorage.removeItem('unsavedActions');
}
- copySchedule(to: number) {
- this.schedule.copySchedule(to);
+ copySchedule(newScheduleName: string) {
+ this.schedule.copySchedule(newScheduleName);
this.unsavedChanges = true;
const action: CopyScheduleAction = {
type: 'copySchedule',
- to: to,
+ newScheduleName: newScheduleName,
};
actionTypesStore.autoSaveSchedule(action);
+ this.emit('scheduleNamesChange');
+ this.emit('currentScheduleIndexChange');
+ this.emit('scheduleNotesChange');
this.emit('addedCoursesChange');
this.emit('customEventsChange');
}
diff --git a/apps/antalmanac/src/stores/Schedules.ts b/apps/antalmanac/src/stores/Schedules.ts
index 0129082ff..9d14a983e 100644
--- a/apps/antalmanac/src/stores/Schedules.ts
+++ b/apps/antalmanac/src/stores/Schedules.ts
@@ -50,10 +50,20 @@ export class Schedules {
this.skeletonSchedules = [];
}
+ getNextScheduleName(newScheduleName: string, scheduleIndex: number) {
+ const scheduleNames = this.getScheduleNames();
+ scheduleNames.splice(scheduleIndex, 1);
+ let nextScheduleName = newScheduleName;
+ let counter = 1;
+
+ while (scheduleNames.includes(nextScheduleName)) {
+ nextScheduleName = `${newScheduleName}(${counter++})`;
+ }
+ return nextScheduleName;
+ }
+
getDefaultScheduleName() {
- const termName = termData[0].shortName.replaceAll(' ', '-');
- const countSameScheduleNames = this.getScheduleNames().filter((name) => name.includes(termName)).length;
- return `${termName + (countSameScheduleNames == 0 ? '' : '(' + countSameScheduleNames + ')')}`;
+ return termData[0].shortName.replaceAll(' ', '-');
}
getCurrentScheduleIndex() {
@@ -92,12 +102,13 @@ export class Schedules {
/**
* Create an empty schedule.
+ * @param newScheduleName The name of the new schedule. If a schedule with the same name already exists, a number will be appended to the name.
*/
addNewSchedule(newScheduleName: string) {
this.addUndoState();
const scheduleNoteId = Math.random();
this.schedules.push({
- scheduleName: newScheduleName,
+ scheduleName: this.getNextScheduleName(newScheduleName, this.getNumberOfSchedules()),
courses: [],
customEvents: [],
scheduleNoteId: scheduleNoteId,
@@ -109,10 +120,11 @@ export class Schedules {
/**
* Rename schedule with the specified index.
+ * @param newScheduleName The name of the new schedule. If a schedule with the same name already exists, a number will be appended to the name.
*/
renameSchedule(newScheduleName: string, scheduleIndex: number) {
this.addUndoState();
- this.schedules[scheduleIndex].scheduleName = newScheduleName;
+ this.schedules[scheduleIndex].scheduleName = this.getNextScheduleName(newScheduleName, scheduleIndex);
}
/**
@@ -134,25 +146,19 @@ export class Schedules {
}
/**
- * Append all courses from current schedule to the schedule with the target index.
- * @param to Index of the schedule to append courses to. If equal to number of schedules, will append courses to all schedules.
+ * Copy the current schedule to a newly created schedule with the specified name.
*/
- copySchedule(to: number) {
- this.addUndoState();
+ copySchedule(newScheduleName: string) {
+ this.addNewSchedule(newScheduleName);
+ this.currentScheduleIndex = this.previousStates[this.previousStates.length - 1].scheduleIndex; // return to previous schedule index for copying
+ const to = this.getNumberOfSchedules() - 1;
+
for (const course of this.getCurrentCourses()) {
- if (to === this.getNumberOfSchedules()) {
- this.addCourseToAllSchedules(course);
- } else {
- this.addCourse(course, to, false);
- }
+ this.addCourse(course, to, false);
}
for (const customEvent of this.getCurrentCustomEvents()) {
- if (to === this.getNumberOfSchedules()) {
- this.addCustomEvent(customEvent, [...Array(to).keys()]);
- } else {
- this.addCustomEvent(customEvent, [to]);
- }
+ this.addCustomEvent(customEvent, [to]);
}
}
diff --git a/apps/backend/drizzle.config.ts b/apps/backend/drizzle.config.ts
index 9eb0a9a76..7e03e4481 100644
--- a/apps/backend/drizzle.config.ts
+++ b/apps/backend/drizzle.config.ts
@@ -1,8 +1,8 @@
import { defineConfig } from "drizzle-kit";
-import env from "./src/env";
+import { rdsEnvSchema } from "./src/env";
-const { DB_URL } = env;
+const { DB_URL } = rdsEnvSchema.parse(process.env);
export default defineConfig({
dialect: "postgresql",
diff --git a/apps/backend/src/db/ddb.ts b/apps/backend/src/db/ddb.ts
index 9bd55d69c..f1536c852 100644
--- a/apps/backend/src/db/ddb.ts
+++ b/apps/backend/src/db/ddb.ts
@@ -6,8 +6,7 @@ import {
UserSchema,
type ScheduleSaveState,
} from '@packages/antalmanac-types';
-
-import env from '../env';
+import {backendEnvSchema} from "../env";
/**
* TODO: enforce this in the schema too, or just leave it as an arbitrary string?
@@ -18,6 +17,8 @@ export const VISIBILITY = {
OPEN: 'open',
};
+const env = backendEnvSchema.parse(process.env);
+
class DDBClient>> {
private tableName: string;
@@ -28,6 +29,10 @@ class DDBClient>> {
documentClient: DynamoDBDocument;
constructor(tableName: string, schema: T) {
+ if (!tableName) {
+ throw new Error('DDBClient(): tableName must be defined');
+ }
+
this.tableName = tableName;
this.schema = schema;
this.client = new DynamoDB({
diff --git a/apps/backend/src/db/index.ts b/apps/backend/src/db/index.ts
index 91c1cf9d4..7314f60dd 100644
--- a/apps/backend/src/db/index.ts
+++ b/apps/backend/src/db/index.ts
@@ -1,12 +1,10 @@
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
-import env from '../env';
+import { rdsEnvSchema } from "../env";
import * as schema from './schema/index.js';
-const { DB_URL } = env;
-
-if (!DB_URL) throw new Error("DB_URL not defined")
+const { DB_URL } = rdsEnvSchema.parse(process.env);
export const client = postgres(DB_URL);
diff --git a/apps/backend/src/env.ts b/apps/backend/src/env.ts
index c0cc84296..c0cc68a58 100644
--- a/apps/backend/src/env.ts
+++ b/apps/backend/src/env.ts
@@ -1,18 +1,41 @@
-import { type } from 'arktype';
-import * as dotenv from 'dotenv';
+import {z} from "zod";
+import * as dotenv from "dotenv";
dotenv.config();
-const Environment = type({
- 'NODE_ENV?': "'development' | 'production' | 'staging'",
- USERDATA_TABLE_NAME: 'string',
- AWS_REGION: 'string',
- MAPBOX_ACCESS_TOKEN: 'string',
- 'PR_NUM?': 'number',
- DB_URL: 'string',
- STAGE: "string",
-});
-const env = Environment.assert({ ...process.env });
+/**
+ * Environment variables required by the backend during deploy time.
+ */
+export const deployEnvSchema = z.object({
+ DB_URL: z.string(),
+ STAGE: z.string(),
+ MAPBOX_ACCESS_TOKEN: z.string(),
+})
-export default env;
+/**
+ * Environment variables required by the backend to connect to the RDS instance.
+ */
+export const rdsEnvSchema = z.object({
+ DB_URL: z.string(),
+ NODE_ENV: z.string().optional(),
+})
+
+/**
+ * Environment variables required by the backend to connect to the DynamoDB table.
+ *
+ * This will be removed once we complete migration to RDS.
+ */
+export const ddbEnvSchema = z.object({
+ USERDATA_TABLE_NAME: z.string(),
+ AWS_REGION: z.string(),
+ NODE_ENV: z.string().optional(),
+})
+
+/**
+ * Environment variables required by the backend during runtime.
+ */
+export const backendEnvSchema = z.intersection(
+ deployEnvSchema,
+ z.intersection(rdsEnvSchema, ddbEnvSchema)
+)
diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts
index 32a591b73..95134c6f0 100644
--- a/apps/backend/src/index.ts
+++ b/apps/backend/src/index.ts
@@ -4,7 +4,7 @@ import type { CorsOptions } from 'cors';
import { createExpressMiddleware } from '@trpc/server/adapters/express';
import AppRouter from './routers';
import createContext from './context';
-import env from './env';
+import { backendEnvSchema } from "./env";
const corsOptions: CorsOptions = {
origin: ['https://antalmanac.com', 'https://www.antalmanac.com', 'https://icssc-projects.github.io/AntAlmanac'],
@@ -15,6 +15,7 @@ const MAPBOX_API_URL = 'https://api.mapbox.com';
const PORT = 3000;
export async function start(corsEnabled = false) {
+ const env = backendEnvSchema.parse(process.env)
const app = express();
app.use(cors(corsEnabled ? corsOptions : undefined));
app.use(express.json());
diff --git a/apps/backend/src/lambda.ts b/apps/backend/src/lambda.ts
index 939892b8c..cff1c76d2 100644
--- a/apps/backend/src/lambda.ts
+++ b/apps/backend/src/lambda.ts
@@ -1,12 +1,13 @@
import serverlessExpress from '@vendia/serverless-express';
import type { Context, Handler } from 'aws-lambda';
-import env from './env';
+import {backendEnvSchema} from './env';
import { start } from '.';
let cachedHandler: Handler;
export async function handler(event: any, context: Context, callback: any) {
+ const env = backendEnvSchema.parse(process.env)
if (!cachedHandler) {
const app = await start(env.NODE_ENV === 'production');
cachedHandler = serverlessExpress({ app });
diff --git a/apps/backend/src/lib/rds.ts b/apps/backend/src/lib/rds.ts
index 355dd59f2..e1a6fa395 100644
--- a/apps/backend/src/lib/rds.ts
+++ b/apps/backend/src/lib/rds.ts
@@ -4,8 +4,7 @@ import { and, eq } from 'drizzle-orm';
import type { Database } from '$db/index';
import {
schedules, users, accounts, coursesInSchedule, customEvents,
- Schedule, CourseInSchedule, CustomEvent,
- AccountType
+ AccountType, Schedule, CourseInSchedule, CustomEvent
} from '$db/schema';
type DatabaseOrTransaction = Omit;
@@ -162,6 +161,66 @@ export class RDS {
);
}
+ /**
+ * Drops all courses in the schedule and re-add them,
+ * deduplicating by section code and term.
+ * */
+ private static async upsertCourses(db: DatabaseOrTransaction, scheduleId: string, courses: ShortCourse[]) {
+ await db.transaction((tx) => tx.delete(coursesInSchedule).where(eq(coursesInSchedule.scheduleId, scheduleId)));
+
+ if (courses.length === 0) {
+ return;
+ }
+
+ const coursesUnique: Set = new Set();
+
+ const dbCourses = courses.map((course) => ({
+ scheduleId,
+ sectionCode: parseInt(course.sectionCode),
+ term: course.term,
+ color: course.color,
+ lastUpdated: new Date(),
+ }));
+
+ const dbCoursesUnique = dbCourses.filter((course) => {
+ const key = `${course.sectionCode}-${course.term}`;
+ if (coursesUnique.has(key)) {
+ return false;
+ }
+ coursesUnique.add(key);
+ return true;
+ });
+
+ await db.transaction((tx) => tx.insert(coursesInSchedule).values(dbCoursesUnique));
+ }
+
+ private static async upsertCustomEvents(
+ db: DatabaseOrTransaction,
+ scheduleId: string,
+ repeatingCustomEvents: RepeatingCustomEvent[]
+ ) {
+ await db.transaction(
+ async (tx) => await tx.delete(customEvents).where(eq(customEvents.scheduleId, scheduleId))
+ );
+
+ if (repeatingCustomEvents.length === 0) {
+ return;
+ }
+
+ const dbCustomEvents = repeatingCustomEvents.map((event) => ({
+ scheduleId,
+ title: event.title,
+ start: event.start,
+ end: event.end,
+ days: event.days.map((day) => (day ? '1' : '0')).join(''),
+ color: event.color,
+ building: event.building,
+ lastUpdated: new Date(),
+ }));
+
+ await db.transaction(async (tx) => await tx.insert(customEvents).values(dbCustomEvents));
+ }
+
static async getGuestUserData(
db: DatabaseOrTransaction, guestId: string
): Promise {
@@ -278,64 +337,4 @@ export class RDS {
// Sort schedules by index
return Object.values(schedulesMapping).sort((a, b) => a.index - b.index);
}
-
- /**
- * Drops all courses in the schedule and re-add them,
- * deduplicating by section code and term.
- * */
- private static async upsertCourses(db: DatabaseOrTransaction, scheduleId: string, courses: ShortCourse[]) {
- await db.transaction((tx) => tx.delete(coursesInSchedule).where(eq(coursesInSchedule.scheduleId, scheduleId)));
-
- if (courses.length === 0) {
- return;
- }
-
- const coursesUnique: Set = new Set();
-
- const dbCourses = courses.map((course) => ({
- scheduleId,
- sectionCode: parseInt(course.sectionCode),
- term: course.term,
- color: course.color,
- lastUpdated: new Date(),
- }));
-
- const dbCoursesUnique = dbCourses.filter((course) => {
- const key = `${course.sectionCode}-${course.term}`;
- if (coursesUnique.has(key)) {
- return false;
- }
- coursesUnique.add(key);
- return true;
- });
-
- await db.transaction((tx) => tx.insert(coursesInSchedule).values(dbCoursesUnique));
- }
-
- private static async upsertCustomEvents(
- db: DatabaseOrTransaction,
- scheduleId: string,
- repeatingCustomEvents: RepeatingCustomEvent[]
- ) {
- await db.transaction(
- async (tx) => await tx.delete(customEvents).where(eq(customEvents.scheduleId, scheduleId))
- );
-
- if (repeatingCustomEvents.length === 0) {
- return;
- }
-
- const dbCustomEvents = repeatingCustomEvents.map((event) => ({
- scheduleId,
- title: event.title,
- start: event.start,
- end: event.end,
- days: event.days.map((day) => (day ? '1' : '0')).join(''),
- color: event.color,
- building: event.building,
- lastUpdated: new Date(),
- }));
-
- await db.transaction(async (tx) => await tx.insert(customEvents).values(dbCustomEvents));
- }
}
diff --git a/apps/backend/src/routers/users.ts b/apps/backend/src/routers/users.ts
index 75aad0e45..73817a84e 100644
--- a/apps/backend/src/routers/users.ts
+++ b/apps/backend/src/routers/users.ts
@@ -1,5 +1,6 @@
import { type } from 'arktype';
+
import { UserSchema } from '@packages/antalmanac-types';
import { db } from 'src/db';
@@ -10,18 +11,6 @@ import { procedure, router } from '../trpc';
const userInputSchema = type([{ userId: 'string' }, '|', { googleId: 'string' }]);
-const viewInputSchema = type({
- /**
- * ID of the user who's requesting to view another user's schedule.
- */
- requesterId: 'string',
-
- /**
- * ID of the user whose schedule is being requested.
- */
- requesteeId: 'string',
-});
-
const saveInputSchema = type({
/**
* ID of the requester.
diff --git a/apps/cdk/package.json b/apps/cdk/package.json
index 36f2f503d..973ad2e54 100644
--- a/apps/cdk/package.json
+++ b/apps/cdk/package.json
@@ -24,7 +24,8 @@
"arktype": "1.0.14-alpha",
"aws-cdk-lib": "^2.94.0",
"constructs": "^10.2.70",
- "dotenv": "^16.0.3"
+ "dotenv": "^16.0.3",
+ "zod": "3.23.8"
},
"devDependencies": {
"@types/node": "^20.11.5",
diff --git a/apps/cdk/src/stacks/backend.ts b/apps/cdk/src/stacks/backend.ts
index 40395d80a..e0d4590aa 100644
--- a/apps/cdk/src/stacks/backend.ts
+++ b/apps/cdk/src/stacks/backend.ts
@@ -1,4 +1,3 @@
-import { type } from 'arktype';
import { Stack, type StackProps, RemovalPolicy, Duration } from 'aws-cdk-lib';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
@@ -7,27 +6,28 @@ import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as route53 from 'aws-cdk-lib/aws-route53';
import * as targets from 'aws-cdk-lib/aws-route53-targets';
import type { Construct } from 'constructs';
+import { z } from 'zod';
+import { deployEnvSchema } from '../../../backend/src/env';
import { zoneName } from '../lib/constants';
export class BackendStack extends Stack {
+ /**
+ * Env vars specifically for the CDK stack/deployment.
+ *
+ * If {@link env.PR_NUM} is defined, then {@link env.NODE_ENV} should be 'staging'.
+ */
+ static readonly CDKEnvironment = z.object({
+ CERTIFICATE_ARN: z.string(),
+ HOSTED_ZONE_ID: z.string(),
+ ANTEATER_API_KEY: z.string(),
+ NODE_ENV: z.string().optional(),
+ PR_NUM: z.string().optional(),
+ });
+
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
-
- /**
- * If {@link env.PR_NUM} is defined, then {@link env.NODE_ENV} should be 'staging'.
- */
- const env = type({
- CERTIFICATE_ARN: 'string',
- HOSTED_ZONE_ID: 'string',
- MONGODB_URI_PROD: 'string',
- GOOGLE_CLIENT_ID: 'string',
- GOOGLE_CLIENT_SECRET: 'string',
- 'MAPBOX_ACCESS_TOKEN?': 'string',
- 'NODE_ENV?': 'string',
- 'PR_NUM?': 'string',
- ANTEATER_API_KEY: 'string',
- }).assert({ ...process.env });
+ const env = z.intersection(BackendStack.CDKEnvironment, deployEnvSchema).parse(process.env);
/**
* The domain that the backend API will be hosted on.
@@ -56,10 +56,7 @@ export class BackendStack extends Stack {
timeout: Duration.seconds(5),
memorySize: 256,
environment: {
- ANTEATER_API_KEY: env.ANTEATER_API_KEY,
- AA_MONGODB_URI: env.MONGODB_URI_PROD,
- MAPBOX_ACCESS_TOKEN: env.MAPBOX_ACCESS_TOKEN ?? '',
- STAGE: env.NODE_ENV ?? 'development',
+ ...env,
USERDATA_TABLE_NAME: userDataDDB.tableName,
},
});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7a103168a..f73333a81 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -34,7 +34,7 @@ importers:
version: 3.3.3
turbo:
specifier: latest
- version: 2.3.1
+ version: 2.3.3
vitest:
specifier: ^0.34.4
version: 0.34.6(jsdom@22.1.0)
@@ -418,6 +418,9 @@ importers:
dotenv:
specifier: ^16.0.3
version: 16.4.5
+ zod:
+ specifier: 3.23.8
+ version: 3.23.8
devDependencies:
'@types/node':
specifier: ^20.11.5
@@ -4858,38 +4861,38 @@ packages:
resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==}
engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'}
- turbo-darwin-64@2.3.1:
- resolution: {integrity: sha512-tjHfjW/Gs8Q9IO+9gPdIsSStZ8I09QYDRT/SyhFTPLnc7O2ZlxHPBVFfjUkHUjanHNYO8CpRGt+zdp1PaMCruw==}
+ turbo-darwin-64@2.3.3:
+ resolution: {integrity: sha512-bxX82xe6du/3rPmm4aCC5RdEilIN99VUld4HkFQuw+mvFg6darNBuQxyWSHZTtc25XgYjQrjsV05888w1grpaA==}
cpu: [x64]
os: [darwin]
- turbo-darwin-arm64@2.3.1:
- resolution: {integrity: sha512-At1WStnxCfrBQ4M2g6ynre8WsusGwA11okhVolBxyFUemYozDTtbZwelr+IqNggjT251vviokxOkcFzzogbiFw==}
+ turbo-darwin-arm64@2.3.3:
+ resolution: {integrity: sha512-DYbQwa3NsAuWkCUYVzfOUBbSUBVQzH5HWUFy2Kgi3fGjIWVZOFk86ss+xsWu//rlEAfYwEmopigsPYSmW4X15A==}
cpu: [arm64]
os: [darwin]
- turbo-linux-64@2.3.1:
- resolution: {integrity: sha512-COwEev7s9fsxLM2eoRCyRLPj+BXvZjFIS+GxzdAubYhoSoZit8B8QGKczyDl6448xhuFEWKrpHhcR9aBuwB4ag==}
+ turbo-linux-64@2.3.3:
+ resolution: {integrity: sha512-eHj9OIB0dFaP6BxB88jSuaCLsOQSYWBgmhy2ErCu6D2GG6xW3b6e2UWHl/1Ho9FsTg4uVgo4DB9wGsKa5erjUA==}
cpu: [x64]
os: [linux]
- turbo-linux-arm64@2.3.1:
- resolution: {integrity: sha512-AP0uE15Rhxza2Jl+Q3gxdXRA92IIeFAYaufz6CMcZuGy9yZsBlLt9w6T47H6g7XQPzWuw8pzfjM1omcTKkkDpQ==}
+ turbo-linux-arm64@2.3.3:
+ resolution: {integrity: sha512-NmDE/NjZoDj1UWBhMtOPmqFLEBKhzGS61KObfrDEbXvU3lekwHeoPvAMfcovzswzch+kN2DrtbNIlz+/rp8OCg==}
cpu: [arm64]
os: [linux]
- turbo-windows-64@2.3.1:
- resolution: {integrity: sha512-HDSneq0dNZYZch74c2eygq+OiJE/JYDs7OsGM0yRYVj336383xkUnxz6W2I7qiyMCQXzp4UVUDZXvZhUYcX3BA==}
+ turbo-windows-64@2.3.3:
+ resolution: {integrity: sha512-O2+BS4QqjK3dOERscXqv7N2GXNcqHr9hXumkMxDj/oGx9oCatIwnnwx34UmzodloSnJpgSqjl8iRWiY65SmYoQ==}
cpu: [x64]
os: [win32]
- turbo-windows-arm64@2.3.1:
- resolution: {integrity: sha512-7/2/sJZiquwoT/jWBCfV0qKq4NarsJPmDRjMcR9dDMIwCYsGM8ljomkDRTCtkNeFcUvYw54MiRWHehWgbcRPsw==}
+ turbo-windows-arm64@2.3.3:
+ resolution: {integrity: sha512-dW4ZK1r6XLPNYLIKjC4o87HxYidtRRcBeo/hZ9Wng2XM/MqqYkAyzJXJGgRMsc0MMEN9z4+ZIfnSNBrA0b08ag==}
cpu: [arm64]
os: [win32]
- turbo@2.3.1:
- resolution: {integrity: sha512-vHZe/e6k1HZVKiMQPQ1BWFn53vjVQDFKdkjUq/pBKlRWi1gw9LQO6ntH4qZCcHY1rH6TXgsRmexXdgWl96YvVQ==}
+ turbo@2.3.3:
+ resolution: {integrity: sha512-DUHWQAcC8BTiUZDRzAYGvpSpGLiaOQPfYXlCieQbwUvmml/LRGIe3raKdrOPOoiX0DYlzxs2nH6BoWJoZrj8hA==}
hasBin: true
type-check@0.4.0:
@@ -7352,8 +7355,8 @@ snapshots:
'@types/react@18.3.12':
dependencies:
- '@types/prop-types': 15.7.5
- csstype: 3.1.1
+ '@types/prop-types': 15.7.13
+ csstype: 3.1.3
'@types/reactcss@1.2.6':
dependencies:
@@ -10322,32 +10325,32 @@ snapshots:
tunnel@0.0.6: {}
- turbo-darwin-64@2.3.1:
+ turbo-darwin-64@2.3.3:
optional: true
- turbo-darwin-arm64@2.3.1:
+ turbo-darwin-arm64@2.3.3:
optional: true
- turbo-linux-64@2.3.1:
+ turbo-linux-64@2.3.3:
optional: true
- turbo-linux-arm64@2.3.1:
+ turbo-linux-arm64@2.3.3:
optional: true
- turbo-windows-64@2.3.1:
+ turbo-windows-64@2.3.3:
optional: true
- turbo-windows-arm64@2.3.1:
+ turbo-windows-arm64@2.3.3:
optional: true
- turbo@2.3.1:
+ turbo@2.3.3:
optionalDependencies:
- turbo-darwin-64: 2.3.1
- turbo-darwin-arm64: 2.3.1
- turbo-linux-64: 2.3.1
- turbo-linux-arm64: 2.3.1
- turbo-windows-64: 2.3.1
- turbo-windows-arm64: 2.3.1
+ turbo-darwin-64: 2.3.3
+ turbo-darwin-arm64: 2.3.3
+ turbo-linux-64: 2.3.3
+ turbo-linux-arm64: 2.3.3
+ turbo-windows-64: 2.3.3
+ turbo-windows-arm64: 2.3.3
type-check@0.4.0:
dependencies: