Skip to content

Commit 066271b

Browse files
committed
:sparkles feat(sub-calendars): update watch collection schema and tests
1 parent 5e871d9 commit 066271b

15 files changed

+268
-427
lines changed

packages/backend/src/__tests__/drivers/util.driver.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
import { WithId } from "mongodb";
1+
import { ObjectId, WithId } from "mongodb";
2+
import { faker } from "@faker-js/faker";
3+
import { Schema_Sync } from "@core/types/sync.types";
24
import { Schema_User } from "@core/types/user.types";
35
import { SyncDriver } from "@backend/__tests__/drivers/sync.driver";
46
import { UserDriver } from "@backend/__tests__/drivers/user.driver";
57
import { WaitListDriver } from "@backend/__tests__/drivers/waitlist.driver";
8+
import mongoService from "@backend/common/services/mongo.service";
69

710
export class UtilDriver {
811
static async setupTestUser(): Promise<{ user: WithId<Schema_User> }> {
@@ -17,4 +20,38 @@ export class UtilDriver {
1720

1821
return { user };
1922
}
23+
24+
static async generateV0SyncData(
25+
numUsers = 3,
26+
): Promise<Array<WithId<Omit<Schema_Sync, "_id">>>> {
27+
const users = await Promise.all(
28+
Array.from({ length: numUsers }, UserDriver.createUser),
29+
);
30+
31+
const data = users.map((user) => ({
32+
_id: new ObjectId(),
33+
user: user._id.toString(),
34+
google: {
35+
events: [
36+
{
37+
resourceId: faker.string.ulid(),
38+
gCalendarId: user.email,
39+
lastSyncedAt: faker.date.past(),
40+
nextSyncToken: faker.string.alphanumeric(32),
41+
channelId: faker.string.uuid(),
42+
expiration: faker.date.future().getTime().toString(),
43+
},
44+
],
45+
calendarlist: [
46+
{
47+
nextSyncToken: faker.string.alphanumeric(32),
48+
gCalendarId: user.email,
49+
lastSyncedAt: faker.date.past(),
50+
},
51+
],
52+
},
53+
}));
54+
55+
return mongoService.sync.insertMany(data).then(() => data);
56+
}
2057
}

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ import {
1111
} from "mongodb";
1212
import { Logger } from "@core/logger/winston.logger";
1313
import {
14-
CompassCalendar,
15-
Schema_CalendarList as Schema_Calendar,
14+
Schema_CalendarList as Schema_CalList,
15+
Schema_Calendar,
1616
} from "@core/types/calendar.types";
1717
import { Schema_Event } from "@core/types/event.types";
1818
import { Schema_Sync } from "@core/types/sync.types";
1919
import { Schema_User } from "@core/types/user.types";
2020
import { Schema_Waitlist } from "@core/types/waitlist/waitlist.types";
21-
import { Watch } from "@core/types/watch.types";
21+
import { Schema_Watch } from "@core/types/watch.types";
2222
import { Collections } from "@backend/common/constants/collections";
2323
import { ENV } from "@backend/common/constants/env.constants";
2424
import { waitUntilEvent } from "@backend/common/helpers/common.util";
@@ -28,13 +28,13 @@ const logger = Logger("app:mongo.service");
2828
interface InternalClient {
2929
db: Db;
3030
client: MongoClient;
31-
calendar: Collection<CompassCalendar>;
32-
calendarList: Collection<Schema_Calendar>;
31+
calendar: Collection<Schema_Calendar>;
32+
calendarList: Collection<Schema_CalList>;
3333
event: Collection<Omit<Schema_Event, "_id">>;
3434
sync: Collection<Schema_Sync>;
3535
user: Collection<Schema_User>;
3636
waitlist: Collection<Schema_Waitlist>;
37-
watch: Collection<Watch>;
37+
watch: Collection<Omit<Schema_Watch, "_id">>;
3838
}
3939

4040
class MongoService {
@@ -138,13 +138,13 @@ class MongoService {
138138
return {
139139
db,
140140
client,
141-
calendar: db.collection<CompassCalendar>(Collections.CALENDAR),
142-
calendarList: db.collection<Schema_Calendar>(Collections.CALENDARLIST),
141+
calendar: db.collection<Schema_Calendar>(Collections.CALENDAR),
142+
calendarList: db.collection<Schema_CalList>(Collections.CALENDARLIST),
143143
event: db.collection<Omit<Schema_Event, "_id">>(Collections.EVENT),
144144
sync: db.collection<Schema_Sync>(Collections.SYNC),
145145
user: db.collection<Schema_User>(Collections.USER),
146146
waitlist: db.collection<Schema_Waitlist>(Collections.WAITLIST),
147-
watch: db.collection<Watch>(Collections.WATCH),
147+
watch: db.collection<Omit<Schema_Watch, "_id">>(Collections.WATCH),
148148
};
149149
}
150150

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ObjectId } from "bson";
12
import { faker } from "@faker-js/faker";
23
import {
34
CompassCalendarSchema,
@@ -65,7 +66,7 @@ describe("Calendar Types", () => {
6566

6667
describe("CompassCalendarSchema", () => {
6768
const compassCalendar = {
68-
_id: faker.database.mongodbObjectId(),
69+
_id: new ObjectId(),
6970
user: faker.database.mongodbObjectId(),
7071
backgroundColor: gCalendar.backgroundColor!,
7172
color: gCalendar.foregroundColor!,

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
IDSchemaV4,
66
RGBHexSchema,
77
TimezoneSchema,
8+
zObjectId,
89
} from "@core/types/type.utils";
910

1011
// @deprecated - will be replaced by Schema_Calendar
@@ -55,7 +56,7 @@ export const GoogleCalendarMetadataSchema = z.object({
5556
});
5657

5758
export const CompassCalendarSchema = z.object({
58-
_id: IDSchemaV4,
59+
_id: zObjectId,
5960
user: IDSchemaV4,
6061
backgroundColor: RGBHexSchema,
6162
color: RGBHexSchema,
@@ -67,4 +68,4 @@ export const CompassCalendarSchema = z.object({
6768
metadata: GoogleCalendarMetadataSchema, // use union when other providers present
6869
});
6970

70-
export type CompassCalendar = z.infer<typeof CompassCalendarSchema>;
71+
export type Schema_Calendar = z.infer<typeof CompassCalendarSchema>;

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ObjectId } from "bson";
22
import { z } from "zod";
33
import { z as zod4 } from "zod/v4";
4+
import { z as zod4Mini } from "zod/v4-mini";
45

56
export type KeyOfType<T, V> = keyof {
67
[P in keyof T as T[P] extends V ? P : never]: unknown;
@@ -19,6 +20,16 @@ export const IDSchemaV4 = zod4.string().refine(ObjectId.isValid, {
1920
message: "Invalid id",
2021
});
2122

23+
export const zObjectIdMini = zod4Mini.pipe(
24+
zod4Mini.custom<ObjectId | string>(ObjectId.isValid),
25+
zod4Mini.transform((v) => new ObjectId(v)),
26+
);
27+
28+
export const zObjectId = zod4.pipe(
29+
zod4.custom<ObjectId | string>((v) => ObjectId.isValid(v as string)),
30+
zod4.transform((v) => new ObjectId(v)),
31+
);
32+
2233
export const TimezoneSchema = zod4.string().refine(
2334
(timeZone) => {
2435
try {

packages/core/src/types/watch.types.test.ts

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
import { ObjectId } from "bson";
12
import { faker } from "@faker-js/faker";
2-
import { Watch, WatchSchema } from "@core/types/watch.types";
3+
import { Schema_Watch, WatchSchema } from "@core/types/watch.types";
34

45
describe("Watch Types", () => {
5-
const validWatch: Watch = {
6-
_id: faker.string.uuid(),
6+
const validWatch: Schema_Watch = {
7+
_id: new ObjectId(),
78
user: faker.database.mongodbObjectId(),
89
resourceId: faker.string.alphanumeric(20),
910
expiration: faker.date.future(),
@@ -48,21 +49,12 @@ describe("Watch Types", () => {
4849

4950
requiredFields.forEach((field) => {
5051
const incompleteWatch = { ...validWatch };
51-
delete incompleteWatch[field as keyof Watch];
52+
delete incompleteWatch[field as keyof Schema_Watch];
5253

5354
expect(() => WatchSchema.parse(incompleteWatch)).toThrow();
5455
});
5556
});
5657

57-
it("accepts string for _id (channelId)", () => {
58-
const watchData = {
59-
...validWatch,
60-
_id: "test-channel-id-123",
61-
};
62-
63-
expect(() => WatchSchema.parse(watchData)).not.toThrow();
64-
});
65-
6658
it("requires expiration to be a Date", () => {
6759
const watchData = {
6860
...validWatch,

packages/core/src/types/watch.types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { z } from "zod/v4";
2-
import { IDSchemaV4 } from "@core/types/type.utils";
2+
import { IDSchemaV4, zObjectId } from "@core/types/type.utils";
33

44
/**
55
* Watch collection schema for Google Calendar push notification channels
@@ -9,7 +9,7 @@ import { IDSchemaV4 } from "@core/types/type.utils";
99
* expiration, deletion) separately from sync data.
1010
*/
1111
export const WatchSchema = z.object({
12-
_id: z.string(), // channel_id - unique identifier for the notification channel
12+
_id: zObjectId, // channel_id - unique identifier for the notification channel
1313
user: IDSchemaV4, // user who owns this watch channel
1414
resourceId: z.string(), // Google Calendar resource identifier
1515
expiration: z.date(), // when the channel expires
@@ -19,4 +19,4 @@ export const WatchSchema = z.object({
1919
.default(() => new Date()), // when this watch was created
2020
});
2121

22-
export type Watch = z.infer<typeof WatchSchema>;
22+
export type Schema_Watch = z.infer<typeof WatchSchema>;

packages/scripts/src/__tests__/integration/2025.10.03T01.19.59.calendar-schema.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ObjectId } from "bson";
12
import { faker } from "@faker-js/faker";
23
import { zodToMongoSchema } from "@scripts/common/zod-to-mongo-schema";
34
import Migration from "@scripts/migrations/2025.10.03T01.19.59.calendar-schema";
@@ -121,7 +122,7 @@ describe("2025.10.03T01.19.59.calendar-schema", () => {
121122
const gCalendar = GoogleCalendarMetadataSchema.parse(gCalendarEntry);
122123

123124
return CompassCalendarSchema.parse({
124-
_id: faker.database.mongodbObjectId(),
125+
_id: new ObjectId(),
125126
user: faker.database.mongodbObjectId(),
126127
backgroundColor: gCalendarEntry.backgroundColor!,
127128
color: gCalendarEntry.foregroundColor!,

packages/scripts/src/__tests__/integration/2025.10.13T14.18.20.watch-collection.test.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import { ObjectId, WithId } from "mongodb";
12
import { faker } from "@faker-js/faker";
23
import { zodToMongoSchema } from "@scripts/common/zod-to-mongo-schema";
34
import Migration from "@scripts/migrations/2025.10.13T14.18.20.watch-collection";
4-
import { Watch, WatchSchema } from "@core/types/watch.types";
5+
import { Schema_Watch, WatchSchema } from "@core/types/watch.types";
56
import {
67
cleanupCollections,
78
cleanupTestDb,
@@ -10,6 +11,8 @@ import {
1011
import { Collections } from "@backend/common/constants/collections";
1112
import mongoService from "@backend/common/services/mongo.service";
1213

14+
type PartialWatch = Partial<WithId<Omit<Schema_Watch, "_id">>>;
15+
1316
describe("2025.10.13T14.18.20.watch-collection", () => {
1417
const migration = new Migration();
1518
const collectionName = Collections.WATCH;
@@ -19,9 +22,9 @@ describe("2025.10.13T14.18.20.watch-collection", () => {
1922
afterEach(() => mongoService.watch.drop());
2023
afterAll(cleanupTestDb);
2124

22-
function generateWatch(): Watch {
25+
function generateWatch(): WithId<Omit<Schema_Watch, "_id">> {
2326
return {
24-
_id: faker.string.uuid(),
27+
_id: new ObjectId(),
2528
user: faker.database.mongodbObjectId(),
2629
resourceId: faker.string.alphanumeric(20),
2730
expiration: faker.date.future(),
@@ -124,8 +127,8 @@ describe("2025.10.13T14.18.20.watch-collection", () => {
124127
it("rejects documents with missing required fields", async () => {
125128
const incompleteWatch = generateWatch();
126129

127-
delete (incompleteWatch as Partial<Watch>).resourceId;
128-
delete (incompleteWatch as Partial<Watch>).expiration;
130+
delete (incompleteWatch as PartialWatch).resourceId;
131+
delete (incompleteWatch as PartialWatch).expiration;
129132

130133
await expect(
131134
mongoService.watch.insertOne(incompleteWatch),
@@ -135,7 +138,7 @@ describe("2025.10.13T14.18.20.watch-collection", () => {
135138
it("rejects documents with missing user", async () => {
136139
const watchWithoutUserId = generateWatch();
137140

138-
delete (watchWithoutUserId as Partial<Watch>).user;
141+
delete (watchWithoutUserId as PartialWatch).user;
139142

140143
await expect(
141144
mongoService.watch.insertOne(watchWithoutUserId),

0 commit comments

Comments
 (0)