Repository: SwitchbackTech/compass
Pull request #215 (2025-01-08 15:55:51)

🐛 Fix: Consistently add `user` field to GridEvent
This PR is a followup to [this PR](https://github.com/SwitchbackTech/compass/pull/214), which address bug #207 

It now adds the `user` field to the `event` object before being submitted to the API to ensure that the `user` field consistently exists inside the object.

It also updates how events are parsed and validated to improve type checking

that-one-arab: @tyler-dane  Thank you for your detailed response. I agree with the points you mentioned, your provided mental model image also helped a lot in solidifying them for me to digest. I'll make the necessary refactors and tag you once done.
that-one-arab: @tyler-dane PR is ready for re-review. I addressed the points you gave, but I had conflicting thoughts about [d8564c6110e7787fe031b0e3607c973aa1b8d9c5](https://github.com/SwitchbackTech/compass/pull/215/commits/d8564c6110e7787fe031b0e3607c973aa1b8d9c5), let me know your thoughts about it and if you think the changes are suitable for the codebase
tyler-dane: @that-one-arab This is looking better, thanks for making those improvements. 

There's a way to make sure both the backend and frontend are validating and using the same type/schema without making the code too complicated. I started implementing it and will push up either a POC or the finished version soon. 

For now, feel free to continue on the other issue you're working on
tyler-dane: Going to do some more thorough QA before merging, but my recent commits demonstrate how we can use zod to make our event objects more type-safe without adding too much technical overhead.
that-one-arab: @tyler-dane Sweet, great work man. I like the type inference from the Zod event validator, this will make maintenance easier

Patch:
diff --git a/jest.config.js b/jest.config.js
index f5ed4caa..1b79b485 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -113,6 +113,7 @@ module.exports = {
         "^@core/(.*)$": "<rootDir>/packages/core/src/$1",
         "^@web/__tests__/(.*)$": "<rootDir>/packages/web/src/__tests__/$1",
         "^@web/assets/(.*)$": "<rootDir>/packages/web/src/assets/$1",
+        "^@web/auth/(.*)$": "<rootDir>/packages/web/src/auth/$1",
         "^@web/common/(.*)$": "<rootDir>/packages/web/src/common/$1",
         "^@web/components/(.*)$": "<rootDir>/packages/web/src/components/$1",
         "^@web/containers/(.*)$": "<rootDir>/packages/web/src/containers/$1",
diff --git a/packages/backend/src/common/errors/handlers/error.express.handler.ts b/packages/backend/src/common/errors/handlers/error.express.handler.ts
index b45d2d81..bdd148ef 100644
--- a/packages/backend/src/common/errors/handlers/error.express.handler.ts
+++ b/packages/backend/src/common/errors/handlers/error.express.handler.ts
@@ -74,8 +74,8 @@ export const handleExpressError = async (
 ) => {
   res.header("Content-Type", "application/json");
 
+  errorHandler.log(e);
   if (e instanceof BaseError) {
-    errorHandler.log(e);
     res.status(e.statusCode).send(e);
   } else {
     const userId = await parseUserId(res, e);
@@ -88,7 +88,7 @@ export const handleExpressError = async (
     }
 
     if (isGoogleError(e)) {
-      await handleGoogleError(userId, res, e as GaxiosError);
+      await handleGoogleError(userId, res, e);
     } else {
       const errInfo = assembleErrorInfo(e);
       res.status(e.status || Status.INTERNAL_SERVER).send(errInfo);
diff --git a/packages/backend/src/event/controllers/event.controller.ts b/packages/backend/src/event/controllers/event.controller.ts
index 849ac50d..973270a2 100644
--- a/packages/backend/src/event/controllers/event.controller.ts
+++ b/packages/backend/src/event/controllers/event.controller.ts
@@ -3,7 +3,9 @@ import {
   Schema_Event,
   Params_DeleteMany,
   Payload_Order,
+  Schema_Event_Core,
 } from "@core/types/event.types";
+import { validateEvent } from "@core/validators/event.validator";
 import { SReqBody, Res_Promise } from "@backend/common/types/express.types";
 import eventService from "@backend/event/services/event.service";
 
@@ -13,12 +15,23 @@ class EventController {
 
     try {
       if (req.body instanceof Array) {
-        const response = await eventService.createMany(req.body);
+        const events = req.body as Schema_Event[];
+        events.forEach(validateEvent);
+
+        const response = await eventService.createMany(
+          events as Schema_Event_Core[]
+        );
         res.promise(response);
         return;
       }
 
-      const response = await eventService.create(userId, req.body);
+      const event = req.body;
+      validateEvent(event);
+
+      const response = await eventService.create(
+        userId,
+        event as Schema_Event_Core
+      );
 
       res.promise(response);
     } catch (e) {
@@ -94,12 +107,14 @@ class EventController {
     const userId = req.session?.getUserId() as string;
     try {
       const event = req.body;
+      validateEvent(event);
+
       const eventId = req.params["id"] as string;
 
       const response = await eventService.updateById(
         userId,
         eventId,
-        event,
+        event as Schema_Event_Core,
         req.query
       );
       res.promise(response);
diff --git a/packages/backend/src/event/queries/event.queries.ts b/packages/backend/src/event/queries/event.queries.ts
index 4997c468..62336eee 100644
--- a/packages/backend/src/event/queries/event.queries.ts
+++ b/packages/backend/src/event/queries/event.queries.ts
@@ -1,5 +1,9 @@
 import { AnyBulkWriteOperation } from "mongodb";
-import { Payload_Order, Schema_Event } from "@core/types/event.types";
+import {
+  Payload_Order,
+  Schema_Event,
+  Schema_Event_Core,
+} from "@core/types/event.types";
 import { Collections } from "@backend/common/constants/collections";
 import { getIdFilter } from "@backend/common/helpers/mongo.utils";
 import mongoService from "@backend/common/services/mongo.service";
@@ -51,7 +55,7 @@ export const reorderEvents = async (userId: string, order: Payload_Order[]) => {
 export const updateEvent = async (
   userId: string,
   eventId: string,
-  event: Schema_Event
+  event: Schema_Event_Core
 ) => {
   const _event = {
     ...event,
diff --git a/packages/backend/src/event/services/event.service.ts b/packages/backend/src/event/services/event.service.ts
index 4442a8f9..d741690a 100644
--- a/packages/backend/src/event/services/event.service.ts
+++ b/packages/backend/src/event/services/event.service.ts
@@ -11,12 +11,13 @@ import { MapEvent } from "@core/mappers/map.event";
 import { BaseError } from "@core/errors/errors.base";
 import { Status } from "@core/errors/status.codes";
 import {
-  Schema_Event,
+  Schema_Event_Core,
   Query_Event,
   Params_DeleteMany,
   Result_DeleteMany,
   Payload_Order,
   Query_Event_Update,
+  Event_Core,
 } from "@core/types/event.types";
 import { getCurrentRangeDates } from "@core/util/date.utils";
 import { Collections } from "@backend/common/constants/collections";
@@ -43,7 +44,7 @@ import {
 } from "../queries/event.queries";
 
 class EventService {
-  create = async (userId: string, event: Schema_Event) => {
+  create = async (userId: string, event: Schema_Event_Core) => {
     const { _event, isRecurring, syncToGcal } = getCreateParams(userId, event);
 
     //  must update gcal's data before Compass's
@@ -66,7 +67,7 @@ class EventService {
       eventId = response.insertedId.toString();
     }
 
-    const eventWithId: Schema_Event = {
+    const eventWithId: Schema_Event_Core = {
       ..._event,
       _id: eventId,
     };
@@ -84,7 +85,7 @@ class EventService {
   createDefaultSomedays = async (userId: string) => {
     const { week, month } = getCurrentRangeDates();
 
-    const defaultWeekly: Schema_Event = {
+    const defaultWeekly: Schema_Event_Core = {
       title: "⭐ That one thing...",
       isAllDay: false,
       isSomeday: true,
@@ -93,13 +94,14 @@ class EventService {
       priority: Priorities.UNASSIGNED,
       origin: Origin.COMPASS,
       description: `... that you wanna do this week, but aren't sure when.\
-        \nKeep it here for safekeeping, then drag it over to the calendar once you're ready to commit times.\
-        \n\nThese sidebar events are:\
-        \n-filtered by the calendar week you're on\
-        \n-limited to ${SOMEDAY_WEEKLY_LIMIT} per week`,
+      \nKeep it here for safekeeping, then drag it over to the calendar once you're ready to commit times.\
+      \n\nThese sidebar events are:\
+      \n-filtered by the calendar week you're on\
+      \n-limited to ${SOMEDAY_WEEKLY_LIMIT} per week`,
+      user: userId,
     };
 
-    const weeklyRepeat: Schema_Event = {
+    const weeklyRepeat: Schema_Event_Core = {
       title: "🪴 Water plants",
       isAllDay: false,
       isSomeday: true,
@@ -115,9 +117,10 @@ class EventService {
       recurrence: {
         rule: [RRULE.WEEK],
       },
+      user: userId,
     };
 
-    const monthlyRepeat: Schema_Event = {
+    const monthlyRepeat: Schema_Event_Core = {
       isAllDay: false,
       isSomeday: true,
       origin: Origin.COMPASS,
@@ -129,6 +132,7 @@ class EventService {
         rule: [RRULE.MONTH],
       },
       description: `This one repeats once a month for ${RRULE_COUNT_MONTHS} months`,
+      user: userId,
     };
 
     await this.create(userId, weeklyRepeat);
@@ -136,7 +140,7 @@ class EventService {
     return await this.create(userId, defaultWeekly);
   };
 
-  createMany = async (events: Schema_Event[]) => {
+  createMany = async (events: Event_Core[]) => {
     const parsedEvents = events.map((e) => {
       const cleanedEvent = {
         ...e,
@@ -185,7 +189,7 @@ class EventService {
     }
 
     const deleteFromGcal = !event["isSomeday"];
-    const _event = event as unknown as Schema_Event;
+    const _event = event as unknown as Schema_Event_Core;
     const gEventId = _event.gEventId;
 
     if (deleteFromGcal) {
@@ -243,7 +247,7 @@ class EventService {
   readAll = async (
     userId: string,
     query: Query_Event
-  ): Promise<Schema_Event[] | BaseError> => {
+  ): Promise<Schema_Event_Core[] | BaseError> => {
     const filter = getReadAllFilter(userId, query);
 
     if (query.someday) {
@@ -252,13 +256,13 @@ class EventService {
         .find(filter)
         .limit(SOMEDAY_WEEKLY_LIMIT + SOMEDAY_MONTHLY_LIMIT)
         .sort({ startDate: 1 })
-        .toArray()) as unknown as Schema_Event[];
+        .toArray()) as unknown as Schema_Event_Core[];
       return response;
     } else {
       const response = (await mongoService.db
         .collection(Collections.EVENT)
         .find(filter)
-        .toArray()) as unknown as Schema_Event[];
+        .toArray()) as unknown as Schema_Event_Core[];
       return response;
     }
   };
@@ -297,7 +301,7 @@ class EventService {
   updateById = async (
     userId: string,
     eventId: string,
-    event: Schema_Event,
+    event: Schema_Event_Core,
     query: Query_Event_Update
   ) => {
     const updateGcal = !event.isSomeday;
@@ -360,7 +364,7 @@ export default new EventService();
  *  to put in event.service.util)
  *********/
 
-const _createGcalEvent = async (userId: string, event: Schema_Event) => {
+const _createGcalEvent = async (userId: string, event: Schema_Event_Core) => {
   const _gEvent = MapEvent.toGcal(event);
 
   const gcal = await getGcalClient(userId);
@@ -369,7 +373,7 @@ const _createGcalEvent = async (userId: string, event: Schema_Event) => {
   return gEvent;
 };
 
-const _createInstances = async (event: Schema_Event, baseId?: string) => {
+const _createInstances = async (event: Schema_Event_Core, baseId?: string) => {
   const instances = assembleInstances(event, baseId);
 
   const { insertedIds } = await mongoService.db
@@ -384,7 +388,7 @@ const _createInstances = async (event: Schema_Event, baseId?: string) => {
   return eventId;
 };
 
-const _deleteFromCompass = async (event: Schema_Event) => {
+const _deleteFromCompass = async (event: Schema_Event_Core) => {
   const isRecurring = event.recurrence !== undefined;
 
   if (!event.user || !event._id) {
@@ -400,14 +404,14 @@ const _deleteFromCompass = async (event: Schema_Event) => {
   return response;
 };
 
-const _removeRecurrence = (event: Schema_Event) => {
+const _removeRecurrence = (event: Schema_Event_Core) => {
   const eventWithoutRecur = { ...event };
   delete eventWithoutRecur.recurrence;
 
   return eventWithoutRecur;
 };
 
-const _updateGcal = async (userId: string, event: Schema_Event) => {
+const _updateGcal = async (userId: string, event: Schema_Event_Core) => {
   const wasSomedayEvent = event.gEventId === undefined;
 
   if (wasSomedayEvent) {
diff --git a/packages/backend/src/event/services/event.service.util.ts b/packages/backend/src/event/services/event.service.util.ts
index 9c73463b..34672352 100644
--- a/packages/backend/src/event/services/event.service.util.ts
+++ b/packages/backend/src/event/services/event.service.util.ts
@@ -7,7 +7,7 @@ import { isSameMonth } from "@core/util/date.utils";
 import {
   Query_Event,
   Query_Event_Update,
-  Schema_Event,
+  Schema_Event_Core,
 } from "@core/types/event.types";
 import { RRULE } from "@core/constants/core.constants";
 import { YEAR_MONTH_DAY_FORMAT } from "@core/constants/date.constants";
@@ -17,7 +17,10 @@ import { GenericError } from "@backend/common/constants/error.constants";
 dayjs.extend(tz);
 dayjs.extend(utc);
 
-export const assembleInstances = (event: Schema_Event, baseId?: string) => {
+export const assembleInstances = (
+  event: Schema_Event_Core,
+  baseId?: string
+) => {
   if (
     !event.recurrence ||
     !event.recurrence.rule ||
@@ -35,7 +38,7 @@ export const assembleInstances = (event: Schema_Event, baseId?: string) => {
   return events;
 };
 
-export const getCreateParams = (userId: string, event: Schema_Event) => {
+export const getCreateParams = (userId: string, event: Schema_Event_Core) => {
   const _event = {
     ...event,
     _id: undefined,
@@ -51,7 +54,9 @@ export const getCreateParams = (userId: string, event: Schema_Event) => {
   return { _event, isRecurring, syncToGcal };
 };
 
-export const getDeleteByIdFilter = (event: Schema_Event): Filter<object> => {
+export const getDeleteByIdFilter = (
+  event: Schema_Event_Core
+): Filter<object> => {
   if (!event._id) {
     throw error(
       GenericError.BadRequest,
@@ -117,7 +122,7 @@ export const getReadAllFilter = (
 };
 
 export const getUpdateAction = (
-  event: Schema_Event,
+  event: Schema_Event_Core,
   query: Query_Event_Update
 ) => {
   const hasInstances = event?.recurrence?.eventId !== undefined;
@@ -234,7 +239,7 @@ const _getDates = (rule: string, nextInstance: Date) => {
 
 const _generateInstances = (
   rule: string,
-  orig: Schema_Event,
+  orig: Schema_Event_Core,
   baseId?: string
 ) => {
   if (!orig.startDate || !orig.endDate) {
diff --git a/packages/core/src/__mocks__/events/gcal/gcal.allday.ts b/packages/core/src/__mocks__/events/gcal/gcal.allday.ts
index ac666f82..8352442c 100644
--- a/packages/core/src/__mocks__/events/gcal/gcal.allday.ts
+++ b/packages/core/src/__mocks__/events/gcal/gcal.allday.ts
@@ -28,7 +28,7 @@ export const allday = [
     sequence: 0,
     private: {
       origin: "googleimport",
-      priority: "relations",
+      priority: "relationships",
     },
 
     reminders: {
diff --git a/packages/core/src/__mocks__/events/gcal/gcal.timed.ts b/packages/core/src/__mocks__/events/gcal/gcal.timed.ts
index 258d8f5f..24c9774c 100644
--- a/packages/core/src/__mocks__/events/gcal/gcal.timed.ts
+++ b/packages/core/src/__mocks__/events/gcal/gcal.timed.ts
@@ -119,7 +119,7 @@ export const timed = [
     extendedProperties: {
       private: {
         origin: "googleimport",
-        priority: "relations",
+        priority: "relationships",
       },
     },
 
diff --git a/packages/core/src/mappers/map.event.ts b/packages/core/src/mappers/map.event.ts
index c4e39446..e53f047c 100644
--- a/packages/core/src/mappers/map.event.ts
+++ b/packages/core/src/mappers/map.event.ts
@@ -2,15 +2,16 @@
 import { BaseError } from "@core/errors/errors.base";
 import { Origin, Priorities, Priority } from "@core/constants/core.constants";
 import { isAllDay, notCancelled } from "@core/util/event.util";
-import { Schema_Event } from "@core/types/event.types";
+import { Event_Core, Schema_Event } from "@core/types/event.types";
 import { gSchema$Event } from "@core/types/gcal";
+import { validateEvent } from "@core/validators/event.validator";
 
 export namespace MapEvent {
   export const toCompass = (
     userId: string,
     events: gSchema$Event[],
     origin?: Origin
-  ): Schema_Event[] => {
+  ): Event_Core[] => {
     const mapped = events
       .filter(notCancelled)
       .map((e: gSchema$Event) => _toCompass(userId, e, origin));
@@ -44,7 +45,7 @@ const _toCompass = (
   userId: string,
   gEvent: gSchema$Event,
   origin?: Origin
-): Schema_Event => {
+): Event_Core => {
   if (!gEvent.id) {
     throw new BaseError(
       "Bad Google Event Id",
@@ -101,5 +102,6 @@ const _toCompass = (
     updatedAt: new Date(),
   };
 
-  return compassEvent;
+  const validatedCompassEvent = validateEvent(compassEvent);
+  return validatedCompassEvent;
 };
diff --git a/packages/core/src/types/event.types.ts b/packages/core/src/types/event.types.ts
index f0bbde16..224a78f5 100644
--- a/packages/core/src/types/event.types.ts
+++ b/packages/core/src/types/event.types.ts
@@ -1,3 +1,4 @@
+import { z } from "zod";
 import { Query } from "express-serve-static-core";
 import { Origin, Priority, Priorities } from "@core/constants/core.constants";
 
@@ -54,10 +55,18 @@ export interface Schema_Event {
   };
   startDate?: string;
   title?: string;
-  updatedAt?: Date;
+  updatedAt?: Date | string;
   user?: string;
 }
 
+export interface Schema_Event_Core extends Schema_Event {
+  startDate: string;
+  endDate: string;
+  origin: Origin;
+  priority: Priority;
+  user: string;
+}
+
 export interface Query_Event extends Query {
   end?: string;
   someday?: string;
@@ -68,3 +77,29 @@ export interface Query_Event extends Query {
 export interface Query_Event_Update extends Query {
   applyTo?: Categories_Recur;
 }
+
+const Recurrence = z.object({
+  rule: z.array(z.string()).optional(),
+  eventId: z.string().optional(),
+});
+
+export const CoreEventSchema = z.object({
+  _id: z.string().optional(),
+  description: z.string().nullable().optional(),
+  endDate: z.union([z.string().datetime({ offset: true }), z.string().date()]),
+  isAllDay: z.boolean().optional(),
+  isSomeday: z.boolean().optional(),
+  gEventId: z.string().optional(),
+  origin: z.nativeEnum(Origin),
+  priority: z.nativeEnum(Priorities),
+  recurrence: Recurrence.optional(),
+  startDate: z.union([
+    z.string().datetime({ offset: true }),
+    z.string().date(),
+  ]),
+  title: z.string().optional(),
+  updatedAt: z.union([z.date(), z.string().datetime()]).optional(),
+  user: z.string(),
+});
+
+export type Event_Core = z.infer<typeof CoreEventSchema>;
diff --git a/packages/core/src/validators/event.validator.ts b/packages/core/src/validators/event.validator.ts
new file mode 100644
index 00000000..8c74346a
--- /dev/null
+++ b/packages/core/src/validators/event.validator.ts
@@ -0,0 +1,6 @@
+import { CoreEventSchema, Schema_Event } from "@core/types/event.types";
+
+export const validateEvent = (event: Schema_Event) => {
+  const result = CoreEventSchema.parse(event);
+  return result;
+};
diff --git a/packages/web/src/common/types/web.event.types.ts b/packages/web/src/common/types/web.event.types.ts
index 9de12cff..2e9540cc 100644
--- a/packages/web/src/common/types/web.event.types.ts
+++ b/packages/web/src/common/types/web.event.types.ts
@@ -1,4 +1,9 @@
-import { Categories_Event, Schema_Event } from "@core/types/event.types";
+import { z } from "zod";
+import {
+  Categories_Event,
+  CoreEventSchema,
+  Schema_Event,
+} from "@core/types/event.types";
 import { SelectOption } from "@web/common/types/component.types";
 
 export enum Recurrence_Selection {
@@ -6,13 +11,17 @@ export enum Recurrence_Selection {
   WEEK = "week",
   MONTH = "month",
 }
+
+export const GridEventSchema = CoreEventSchema.extend({
+  hasFlipped: z.boolean().optional(),
+  isOpen: z.boolean().optional(),
+  row: z.number().optional(),
+});
+
 export interface Schema_GridEvent extends Schema_Event {
   hasFlipped?: boolean;
-  importanceIndex?: number;
-  isEditing?: boolean;
   isOpen?: boolean;
-  row?: number;
-  siblingsCount?: number;
+  row?: number; //TODO: delete if not used
 }
 
 export interface Schema_OptimisticEvent extends Schema_Event {
diff --git a/packages/web/src/common/utils/event.util.ts b/packages/web/src/common/utils/event.util.ts
index 76017fef..2df669bf 100644
--- a/packages/web/src/common/utils/event.util.ts
+++ b/packages/web/src/common/utils/event.util.ts
@@ -7,18 +7,17 @@ import { DropResult } from "@hello-pangea/dnd";
 import { YEAR_MONTH_DAY_COMPACT_FORMAT } from "@core/constants/date.constants";
 import { Categories_Event, Schema_Event } from "@core/types/event.types";
 import { Origin, Priorities } from "@core/constants/core.constants";
+import { validateEvent } from "@core/validators/event.validator";
 import { Status } from "@core/errors/status.codes";
-import { Response_GetEventsSaga } from "@web/ducks/events/event.types";
 
 import {
   Schema_GridEvent,
   Schema_OptimisticEvent,
   Schema_SomedayEventsColumn,
 } from "../types/web.event.types";
-import { removeGridProperties } from "./grid.util";
 import {
-  COLUMN_WEEK,
   COLUMN_MONTH,
+  COLUMN_WEEK,
   ID_OPTIMISTIC_PREFIX,
 } from "../constants/web.constants";
 
@@ -141,14 +140,15 @@ export const handleError = (error: Error) => {
     return;
   }
 
+  console.log(error.message);
+  console.log(error.stack);
+  console.log(error);
+
   if (code === Status.INTERNAL_SERVER) {
     alert("Something went wrong behind the scenes. Please try again later.");
     window.location.reload();
   }
 
-  console.log(error.message);
-  console.log(error.stack);
-  console.log(error);
   alert(error);
 };
 
@@ -192,24 +192,17 @@ export const prepEvtAfterDraftDrop = (
   return event;
 };
 
-export const prepEvtBeforeConvertToSomeday = (draft: Schema_GridEvent) => {
-  const event = removeGridProperties(draft);
-
-  if (event.recurrence) {
-    delete event.recurrence;
-  }
-
-  return event;
-};
-
-export const prepEvtBeforeSubmit = (draft: Schema_GridEvent) => {
-  const _event = removeGridProperties({ ...draft });
-
-  const event = {
-    ..._event,
+export const prepEvtBeforeSubmit = (
+  draft: Schema_GridEvent,
+  userId: string
+) => {
+  const _event = {
+    ...draft,
     origin: Origin.COMPASS,
-  } as Schema_Event;
+    user: userId,
+  };
 
+  const event = validateEvent(_event);
   return event;
 };
 
diff --git a/packages/web/src/common/utils/grid.util.ts b/packages/web/src/common/utils/grid.util.ts
index 29718680..274dade5 100644
--- a/packages/web/src/common/utils/grid.util.ts
+++ b/packages/web/src/common/utils/grid.util.ts
@@ -18,7 +18,6 @@ import {
   GRID_X_PADDING_TOTAL,
   SIDEBAR_OPEN_WIDTH,
 } from "@web/views/Calendar/layout.constants";
-import { Schema_GridEvent } from "@web/common/types/web.event.types";
 
 dayjs.extend(dayOfYear);
 dayjs.extend(weekPlugin);
@@ -488,19 +487,6 @@ const normalizeDayNums = (days: number[]) => {
   });
 };
 
-export const removeGridProperties = (event: Schema_GridEvent): Schema_Event => {
-  const {
-    isEditing,
-    importanceIndex,
-    isOpen,
-    row,
-    siblingsCount,
-    ...eventWithoutGridProps
-  } = event;
-
-  return eventWithoutGridProps;
-};
-
 export const removeSomedayProperties = (event: Schema_Event): Schema_Event => {
   const { order, recurrence, ...eventWithoutSomedayProps } = event;
   return eventWithoutSomedayProps;
diff --git a/packages/web/src/common/validators/grid.event.validator.ts b/packages/web/src/common/validators/grid.event.validator.ts
new file mode 100644
index 00000000..193cc047
--- /dev/null
+++ b/packages/web/src/common/validators/grid.event.validator.ts
@@ -0,0 +1,8 @@
+import { Schema_Event } from "@core/types/event.types";
+
+import { GridEventSchema } from "../types/web.event.types";
+
+export const validateGridEvent = (event: Schema_Event) => {
+  const result = GridEventSchema.parse(event);
+  return result;
+};
diff --git a/packages/web/src/ducks/events/sagas/somday.sagas.ts b/packages/web/src/ducks/events/sagas/somday.sagas.ts
index 5e62f1ab..5627113a 100644
--- a/packages/web/src/ducks/events/sagas/somday.sagas.ts
+++ b/packages/web/src/ducks/events/sagas/somday.sagas.ts
@@ -8,7 +8,7 @@ import {
   handleError,
 } from "@web/common/utils/event.util";
 import { Schema_GridEvent } from "@web/common/types/web.event.types";
-import { removeSomedayProperties } from "@web/common/utils/grid.util";
+import { validateGridEvent } from "@web/common/validators/grid.event.validator";
 
 import { Action_Someday_Reorder } from "../slices/someday.slice.types";
 import { EventApi } from "../event.api";
@@ -101,8 +101,8 @@ function* _assembleGridEvent(
   const currEvent = yield* getEventById(_id);
 
   const _gridEvent = { ...currEvent, ...updatedFields };
-  const gridEvent = removeSomedayProperties(_gridEvent);
-  return gridEvent as Schema_GridEvent;
+  const gridEvent = validateGridEvent(_gridEvent);
+  return gridEvent;
 }
 
 function* _convertEvent(gridEvent: Schema_GridEvent) {
diff --git a/packages/web/src/views/Calendar/hooks/draft/sidebar/useSidebarUtil.ts b/packages/web/src/views/Calendar/hooks/draft/sidebar/useSidebarUtil.ts
index 2f237d6d..df9d5a2d 100644
--- a/packages/web/src/views/Calendar/hooks/draft/sidebar/useSidebarUtil.ts
+++ b/packages/web/src/views/Calendar/hooks/draft/sidebar/useSidebarUtil.ts
@@ -12,6 +12,7 @@ import {
 } from "@core/types/event.types";
 import { DropResult } from "@hello-pangea/dnd";
 import { ID_SOMEDAY_DRAFT } from "@web/common/constants/web.constants";
+import { getUserId } from "@web/auth/auth.util";
 import { DropResult_ReactDND } from "@web/common/types/dnd.types";
 import { Schema_GridEvent } from "@web/common/types/web.event.types";
 import {
@@ -283,15 +284,18 @@ export const useSidebarUtil = (
     close();
   };
 
-  const onSubmit = (category: Categories_Event) => {
-    const _event = prepEvtBeforeSubmit(state.draft);
+  const onSubmit = async (category: Categories_Event) => {
+    if (!state.draft) return;
 
     const { startDate, endDate } = getDatesByCategory(
       category,
       viewStart,
       viewEnd
     );
-    const event = { ..._event, startDate, endDate };
+    const _event = { ...state.draft, startDate, endDate };
+
+    const userId = await getUserId();
+    const event = prepEvtBeforeSubmit(_event, userId);
 
     const isExisting = event._id;
     if (isExisting) {
diff --git a/packages/web/src/views/Calendar/hooks/draft/useDraftUtil.ts b/packages/web/src/views/Calendar/hooks/draft/useDraftUtil.ts
index 6fe688dc..63a70b7f 100644
--- a/packages/web/src/views/Calendar/hooks/draft/useDraftUtil.ts
+++ b/packages/web/src/views/Calendar/hooks/draft/useDraftUtil.ts
@@ -1,4 +1,4 @@
-import { MouseEvent } from "react";
+import { MouseEvent, useCallback, useEffect, useState } from "react";
 import dayjs, { Dayjs } from "dayjs";
 import {
   Priorities,
@@ -6,6 +6,7 @@ import {
 } from "@core/constants/core.constants";
 import { YEAR_MONTH_DAY_FORMAT } from "@core/constants/date.constants";
 import { useAppDispatch, useAppSelector } from "@web/store/store.hooks";
+import { validateEvent } from "@core/validators/event.validator";
 import { Schema_GridEvent } from "@web/common/types/web.event.types";
 import { getX } from "@web/common/utils/grid.util";
 import {
@@ -15,10 +16,8 @@ import {
 } from "@web/ducks/events/slices/event.slice";
 import { getWeekEventsSlice } from "@web/ducks/events/slices/week.slice";
 import { draftSlice } from "@web/ducks/events/slices/draft.slice";
-import { useCallback, useEffect, useState } from "react";
 import {
   getDefaultEvent,
-  prepEvtBeforeConvertToSomeday,
   prepEvtBeforeSubmit,
 } from "@web/common/utils/event.util";
 import {
@@ -30,6 +29,7 @@ import {
   selectIsAtWeeklyLimit,
   selectSomedayWeekCount,
 } from "@web/ducks/events/selectors/someday.selectors";
+import { getUserId } from "@web/auth/auth.util";
 
 import { DateCalcs } from "../grid/useDateCalcs";
 import { WeekProps } from "../useWeek";
@@ -164,7 +164,7 @@ export const useDraftUtil = (
       endDate: end,
       order: somedayWeekCount,
     };
-    const event = prepEvtBeforeConvertToSomeday(_draft);
+    const event = validateEvent(_draft);
     dispatch(getWeekEventsSlice.actions.convert({ event }));
 
     discard();
@@ -375,8 +375,9 @@ export const useDraftUtil = (
     setDateBeingChanged("endDate");
   };
 
-  const submit = (draft: Schema_GridEvent) => {
-    const event = prepEvtBeforeSubmit(draft);
+  const submit = async (draft: Schema_GridEvent) => {
+    const userId = await getUserId();
+    const event = prepEvtBeforeSubmit(draft, userId);
     const { startOfView, endOfView } = weekProps.component;
 
     const isExisting = event._id;


Test patch:
diff --git a/packages/core/src/validators/event.validator.test.ts b/packages/core/src/validators/event.validator.test.ts
new file mode 100644
index 00000000..8c4471c0
--- /dev/null
+++ b/packages/core/src/validators/event.validator.test.ts
@@ -0,0 +1,77 @@
+import { validateEvent } from "./event.validator";
+import { Origin, Priorities } from "../constants/core.constants";
+import { Schema_Event } from "../types/event.types";
+
+describe("validateEvent", () => {
+  it("strips unexpected properties", () => {
+    const event = {
+      startDate: "2023-01-01",
+      endDate: "2023-01-02",
+      origin: Origin.COMPASS,
+      priority: Priorities.RELATIONS,
+      user: "user123",
+      unexpectedProperty: "unexpectedValue",
+    };
+
+    const parsedEvent = validateEvent(event);
+    expect(parsedEvent).not.toHaveProperty("unexpectedProperty");
+  });
+  it("validates a correct event", () => {
+    const event: Schema_Event = {
+      startDate: "2023-01-01",
+      endDate: "2023-01-02",
+      origin: Origin.COMPASS,
+      priority: Priorities.RELATIONS,
+      user: "user123",
+    };
+
+    const parsedEvent = validateEvent(event);
+    expect(parsedEvent).toEqual(event);
+  });
+
+  it("invalidates when properties are missing", () => {
+    const event = {
+      startDate: "2023-01-01",
+      endDate: "2023-01-02",
+      origin: Origin.GOOGLE,
+    };
+
+    expect(() => validateEvent(event)).toThrow();
+  });
+
+  it("invalidates when types are incorrect", () => {
+    const event = {
+      startDate: 20230101,
+      endDate: "2023-01-02",
+      origin: "INVALID_ORIGIN",
+      priority: "INVALID_PRIORITY",
+      user: 123,
+    } as unknown as Schema_Event;
+
+    expect(() => validateEvent(event)).toThrow();
+  });
+
+  it("invalidates when date format is invalid", () => {
+    const event: Schema_Event = {
+      startDate: "01-01-2023", // wrong format
+      endDate: "2023-02-21",
+      origin: Origin.GOOGLE_IMPORT,
+      priority: Priorities.WORK,
+      user: "user123",
+    };
+
+    expect(() => validateEvent(event)).toThrow();
+  });
+});
+
+it("invalidates when datetime format is invalid", () => {
+  const event: Schema_Event = {
+    startDate: "2023-01-01T05:00:00", //missing offset
+    endDate: "2023-01-02T15:00:00+02:00",
+    origin: Origin.COMPASS,
+    priority: Priorities.RELATIONS,
+    user: "user123",
+  };
+
+  expect(() => validateEvent(event)).toThrow();
+});
diff --git a/packages/web/src/common/validators/grid.event.validator.test.ts b/packages/web/src/common/validators/grid.event.validator.test.ts
new file mode 100644
index 00000000..f457c250
--- /dev/null
+++ b/packages/web/src/common/validators/grid.event.validator.test.ts
@@ -0,0 +1,48 @@
+import { Origin, Priorities } from "@core/constants/core.constants";
+
+import { Schema_GridEvent } from "../types/web.event.types";
+import { validateGridEvent } from "./grid.event.validator";
+
+describe("validateGridEvent", () => {
+  it("strips unexpected properties", () => {
+    const event = {
+      startDate: "2023-01-01",
+      endDate: "2023-01-02",
+      origin: Origin.COMPASS,
+      priority: Priorities.RELATIONS,
+      user: "user123",
+      unexpectedProperty: "unexpectedValue",
+    };
+
+    const parsedEvent = validateGridEvent(event);
+    expect(parsedEvent).not.toHaveProperty("unexpectedProperty");
+  });
+  it("validates a correct event", () => {
+    const event: Schema_GridEvent = {
+      endDate: "2023-01-02",
+      hasFlipped: true,
+      isOpen: true,
+      row: 10,
+      startDate: "2023-01-01",
+      origin: Origin.COMPASS,
+      priority: Priorities.RELATIONS,
+      user: "user123",
+    };
+
+    const parsedEvent = validateGridEvent(event);
+    expect(parsedEvent).toEqual(event);
+  });
+
+  it("invalidates when types are incorrect", () => {
+    const event = {
+      startDate: "2022-10-22",
+      endDate: "2023-01-02",
+      origin: Origin.UNSURE,
+      priority: Priorities.SELF,
+      user: "user1",
+      hasFlipped: "true", // invalid
+    } as unknown as Schema_GridEvent;
+
+    expect(() => validateGridEvent(event)).toThrow();
+  });
+});
diff --git a/packages/web/src/views/Calendar/Calendar.form.test.tsx b/packages/web/src/views/Calendar/Calendar.form.test.tsx
index 36679f5b..dfedb548 100644
--- a/packages/web/src/views/Calendar/Calendar.form.test.tsx
+++ b/packages/web/src/views/Calendar/Calendar.form.test.tsx
@@ -1,9 +1,8 @@
 import React, { act } from "react";
 import "@testing-library/jest-dom";
 import { screen, waitFor, within } from "@testing-library/react";
-import userEvent from "@testing-library/user-event";
-import { UserEvent } from "@testing-library/user-event/dist/types/setup/setup";
-import { CLIMB, MULTI_WEEK, TY_TIM } from "@core/__mocks__/events/events.misc";
+import userEvent, { UserEvent } from "@testing-library/user-event";
+import { CLIMB } from "@core/__mocks__/events/events.misc";
 import { CalendarView } from "@web/views/Calendar";
 import { render } from "@web/__tests__/__mocks__/mock.render";
 import { preloadedState } from "@web/__tests__/__mocks__/state/state.weekEvents";