Skip to content

Commit b0b52cd

Browse files
✨ feat(sub-calendars): create calendar collection and schema (#1067)
1 parent d227d8d commit b0b52cd

File tree

17 files changed

+961
-62
lines changed

17 files changed

+961
-62
lines changed

packages/backend/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@
2323
"saslprep": "^1.0.3",
2424
"socket.io": "^4.7.5",
2525
"supertokens-node": "^20.0.5",
26-
"tslib": "^2.4.0",
27-
"zod": "^3.24.1"
26+
"tslib": "^2.4.0"
2827
},
2928
"devDependencies": {
3029
"@faker-js/faker": "^9.6.0",

packages/backend/src/calendar/services/calendar.service.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class CalendarService {
99
) => {
1010
const payload = calendarList.google.items;
1111

12-
const response = await mongoService.calendar.updateOne(
12+
const response = await mongoService.calendarList.updateOne(
1313
{ user: userId },
1414
{ $push: { [`${integration}.items`]: payload } },
1515
{ upsert: true },
@@ -19,18 +19,18 @@ class CalendarService {
1919
};
2020

2121
create = async (calendarList: Schema_CalendarList) => {
22-
return await mongoService.calendar.insertOne(calendarList);
22+
return await mongoService.calendarList.insertOne(calendarList);
2323
};
2424

2525
async deleteAllByUser(userId: string) {
2626
const filter = { user: userId };
27-
const response = await mongoService.calendar.deleteMany(filter);
27+
const response = await mongoService.calendarList.deleteMany(filter);
2828

2929
return response;
3030
}
3131

3232
deleteByIntegrateion = async (integration: "google", userId: string) => {
33-
const response = await mongoService.calendar.updateOne(
33+
const response = await mongoService.calendarList.updateOne(
3434
{ user: userId },
3535
{ $unset: { [`${integration}.items`]: "" } },
3636
);

packages/backend/src/common/constants/collections.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { IS_DEV } from "./env.constants";
1+
import { IS_DEV } from "@backend/common/constants/env.constants";
22

33
export const Collections = {
4+
CALENDAR: IS_DEV ? "_dev.calendar" : "calendar",
45
CALENDARLIST: IS_DEV ? "_dev.calendarlist" : "calendarlist",
56
EVENT: IS_DEV ? "_dev.event" : "event",
67
OAUTH: IS_DEV ? "_dev.oauth" : "oauth",

packages/backend/src/common/services/mongo.service.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,25 @@ import {
1010
ObjectId,
1111
} from "mongodb";
1212
import { Logger } from "@core/logger/winston.logger";
13-
import { Schema_CalendarList as Schema_Calendar } from "@core/types/calendar.types";
13+
import {
14+
CompassCalendar,
15+
Schema_CalendarList as Schema_Calendar,
16+
} from "@core/types/calendar.types";
1417
import { Schema_Event } from "@core/types/event.types";
1518
import { Schema_Sync } from "@core/types/sync.types";
1619
import { Schema_User } from "@core/types/user.types";
1720
import { Schema_Waitlist } from "@core/types/waitlist/waitlist.types";
21+
import { Collections } from "@backend/common/constants/collections";
22+
import { ENV } from "@backend/common/constants/env.constants";
1823
import { waitUntilEvent } from "@backend/common/helpers/common.util";
19-
import { Collections } from "../constants/collections";
20-
import { ENV } from "../constants/env.constants";
2124

2225
const logger = Logger("app:mongo.service");
2326

2427
interface InternalClient {
2528
db: Db;
2629
client: MongoClient;
27-
calendar: Collection<Schema_Calendar>;
30+
calendar: Collection<CompassCalendar>;
31+
calendarList: Collection<Schema_Calendar>;
2832
event: Collection<Omit<Schema_Event, "_id">>;
2933
sync: Collection<Schema_Sync>;
3034
user: Collection<Schema_User>;
@@ -47,6 +51,15 @@ class MongoService {
4751
return this.#accessInternalCollectionProps("calendar");
4852
}
4953

54+
/**
55+
* calendarList
56+
*
57+
* mongo collection
58+
*/
59+
get calendarList(): InternalClient["calendarList"] {
60+
return this.#accessInternalCollectionProps("calendarList");
61+
}
62+
5063
/**
5164
* event
5265
*
@@ -114,7 +127,8 @@ class MongoService {
114127
return {
115128
db,
116129
client,
117-
calendar: db.collection<Schema_Calendar>(Collections.CALENDARLIST),
130+
calendar: db.collection<CompassCalendar>(Collections.CALENDAR),
131+
calendarList: db.collection<Schema_Calendar>(Collections.CALENDARLIST),
118132
event: db.collection<Omit<Schema_Event, "_id">>(Collections.EVENT),
119133
sync: db.collection<Schema_Sync>(Collections.SYNC),
120134
user: db.collection<Schema_User>(Collections.USER),
@@ -206,6 +220,10 @@ class MongoService {
206220
const r = await this.db.collection(collection).findOne(filter);
207221
return r !== null;
208222
}
223+
224+
async collectionExists(name: string): Promise<boolean> {
225+
return this.db.listCollections({ name }).hasNext();
226+
}
209227
}
210228

211229
export default new MongoService();

packages/core/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"dependencies": {
1414
"bson": "^6.10.4",
1515
"tinycolor2": "^1.6.0",
16-
"winston": "^3.8.1"
16+
"winston": "^3.8.1",
17+
"zod": "^3.25.76"
1718
}
1819
}

packages/core/src/__mocks__/v1/calendarlist/gcal.calendarlist.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
export const gcalCalendarList = {
1+
import { gSchema$CalendarList } from "@core/types/gcal";
2+
3+
export const gcalCalendarList: gSchema$CalendarList = {
24
kind: "calendar#calendarList",
35
etag: '"p32g9nb5bt6fva0g"',
46
nextSyncToken: "CKCbrKvpn_UCEhh0eWxlci5oaXR6ZW1hbkBnbWFpbC5jb20=",

packages/core/src/types/calendar.types.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
import { z } from "zod";
1+
import { z } from "zod/v4";
2+
import { CalendarProvider } from "@core/types/event.types";
23
import { gSchema$CalendarListEntry } from "@core/types/gcal";
3-
import { IDSchema, RGBHexSchema, TimezoneSchema } from "@core/types/type.utils";
4-
import { CalendarProvider } from "./event.types";
4+
import {
5+
IDSchemaV4,
6+
RGBHexSchema,
7+
TimezoneSchema,
8+
} from "@core/types/type.utils";
59

610
// @deprecated - will be replaced by Schema_Calendar
711
export interface Schema_CalendarList {
@@ -20,7 +24,7 @@ export const GoogleCalendarMetadataSchema = z.object({
2024
description: z.string().nullable().optional(),
2125
location: z.string().nullable().optional(),
2226
accessRole: z.enum(["freeBusyReader", "reader", "writer", "owner"]),
23-
primary: z.boolean().default(false),
27+
primary: z.boolean().default(false).optional(),
2428
conferenceProperties: z.object({
2529
allowedConferenceSolutionTypes: z.array(
2630
z.enum(["hangoutsMeet", "eventHangout", "eventNamedHangout"]),
@@ -51,15 +55,15 @@ export const GoogleCalendarMetadataSchema = z.object({
5155
});
5256

5357
export const CompassCalendarSchema = z.object({
54-
_id: IDSchema,
55-
user: IDSchema,
58+
_id: IDSchemaV4,
59+
user: IDSchemaV4,
5660
backgroundColor: RGBHexSchema,
5761
color: RGBHexSchema,
5862
selected: z.boolean().default(true),
5963
primary: z.boolean().default(false),
60-
timezone: TimezoneSchema.optional(),
64+
timezone: TimezoneSchema.nullable().optional(),
6165
createdAt: z.date().default(() => new Date()),
62-
updatedAt: z.date().optional(),
66+
updatedAt: z.date().nullable().optional(),
6367
metadata: GoogleCalendarMetadataSchema, // use union when other providers present
6468
});
6569

packages/core/src/types/type.utils.test.ts

Lines changed: 37 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1-
import { ZodError } from "zod";
1+
import { ZodError as ZodErrorV3 } from "zod";
2+
import { ZodError, z } from "zod/v4";
23
import { faker } from "@faker-js/faker";
3-
import { IDSchema, RGBHexSchema, TimezoneSchema } from "@core/types/type.utils";
4+
import {
5+
IDSchema,
6+
IDSchemaV4,
7+
RGBHexSchema,
8+
TimezoneSchema,
9+
} from "@core/types/type.utils";
410

511
describe("IDSchema", () => {
612
it("validates a correct ObjectId string", () => {
@@ -13,11 +19,30 @@ describe("IDSchema", () => {
1319
const invalidId = faker.string.ulid();
1420
const result = IDSchema.safeParse(invalidId);
1521

22+
expect(result.success).toBe(false);
23+
expect(result.error).toBeInstanceOf(ZodErrorV3);
24+
expect(result.error?.errors).toEqual(
25+
expect.arrayContaining([
26+
expect.objectContaining({ message: "Invalid id" }),
27+
]),
28+
);
29+
});
30+
});
31+
32+
describe("IDSchemaV4", () => {
33+
it("validates a correct ObjectId string", () => {
34+
const validId = faker.database.mongodbObjectId();
35+
36+
expect(IDSchemaV4.safeParse(validId).success).toBe(true);
37+
});
38+
39+
it("rejects an invalid ObjectId string", () => {
40+
const invalidId = faker.string.ulid();
41+
const result = IDSchemaV4.safeParse(invalidId);
42+
1643
expect(result.success).toBe(false);
1744
expect(result.error).toBeInstanceOf(ZodError);
18-
expect(result.error?.errors).toEqual([
19-
{ message: "Invalid id", path: [], code: "custom" },
20-
]);
45+
expect(z.treeifyError(result.error!).errors).toEqual(["Invalid id"]);
2146
});
2247
});
2348

@@ -34,9 +59,7 @@ describe("TimezoneSchema", () => {
3459

3560
expect(result.success).toBe(false);
3661
expect(result.error).toBeInstanceOf(ZodError);
37-
expect(result.error?.errors).toEqual([
38-
{ message: "Invalid timezone", path: [], code: "custom" },
39-
]);
62+
expect(z.treeifyError(result.error!).errors).toEqual(["Invalid timezone"]);
4063
});
4164

4265
describe("RGBHexSchema", () => {
@@ -58,13 +81,8 @@ describe("TimezoneSchema", () => {
5881

5982
expect(result.success).toBe(false);
6083
expect(result.error).toBeInstanceOf(ZodError);
61-
expect(result.error?.errors).toEqual([
62-
{
63-
message: "Invalid color. Must be a 7-character hex color code.",
64-
path: [],
65-
code: "invalid_string",
66-
validation: "regex",
67-
},
84+
expect(z.treeifyError(result.error!).errors).toEqual([
85+
"Invalid color. Must be a 7-character hex color code.",
6886
]);
6987
});
7088

@@ -74,13 +92,8 @@ describe("TimezoneSchema", () => {
7492

7593
expect(result.success).toBe(false);
7694
expect(result.error).toBeInstanceOf(ZodError);
77-
expect(result.error?.errors).toEqual([
78-
{
79-
message: "Invalid color. Must be a 7-character hex color code.",
80-
path: [],
81-
code: "invalid_string",
82-
validation: "regex",
83-
},
95+
expect(z.treeifyError(result.error!).errors).toEqual([
96+
"Invalid color. Must be a 7-character hex color code.",
8497
]);
8598
});
8699

@@ -90,13 +103,8 @@ describe("TimezoneSchema", () => {
90103

91104
expect(result.success).toBe(false);
92105
expect(result.error).toBeInstanceOf(ZodError);
93-
expect(result.error?.errors).toEqual([
94-
{
95-
message: "Invalid color. Must be a 7-character hex color code.",
96-
path: [],
97-
code: "invalid_string",
98-
validation: "regex",
99-
},
106+
expect(z.treeifyError(result.error!).errors).toEqual([
107+
"Invalid color. Must be a 7-character hex color code.",
100108
]);
101109
});
102110
});

packages/core/src/types/type.utils.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ObjectId } from "bson";
22
import { z } from "zod";
3+
import { z as zod4 } from "zod/v4";
34

45
export type KeyOfType<T, V> = keyof {
56
[P in keyof T as T[P] extends V ? P : never]: unknown;
@@ -14,7 +15,11 @@ export const IDSchema = z.string().refine(ObjectId.isValid, {
1415
message: "Invalid id",
1516
});
1617

17-
export const TimezoneSchema = z.string().refine(
18+
export const IDSchemaV4 = zod4.string().refine(ObjectId.isValid, {
19+
message: "Invalid id",
20+
});
21+
22+
export const TimezoneSchema = zod4.string().refine(
1823
(timeZone) => {
1924
try {
2025
new Intl.DateTimeFormat("en-US", { timeZone });
@@ -28,6 +33,6 @@ export const TimezoneSchema = z.string().refine(
2833
{ message: "Invalid timezone" },
2934
);
3035

31-
export const RGBHexSchema = z.string().regex(/^#[0-9a-f]{6}$/i, {
36+
export const RGBHexSchema = zod4.string().regex(/^#[0-9a-f]{6}$/i, {
3237
message: "Invalid color. Must be a 7-character hex color code.",
3338
});

0 commit comments

Comments
 (0)