Skip to content

Commit 36a91d2

Browse files
authored
✨ Feat: add support for UPDATING INSTANCE and STANDALONE events (#366)
* ♻️ Refactor: rename mock gcal utils for clarity * ♻️ Refactor: skip tracking standalone events in map during full import Tracking them in the map was unnecessary, because were won't come back again in subsequent passes (because there's only one) * ♻️ Refactor: move mock event untils inside @backend * ✨ Feat: add support for UPDATING and INSTANCE * ✨ Feat: setup the EventRepository This is similar to the RecurringEventReposity, but is focused on operations that apply to regular events * ✨ Feat: add support for UPSERTING a new or existing standalone event
1 parent 610956e commit 36a91d2

21 files changed

+367
-152
lines changed

packages/backend/src/__tests__/helpers/mock.events.init.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import {
77
gSchema$EventBase,
88
gSchema$EventInstance,
99
} from "@core/types/gcal";
10-
import { isBase } from "@core/util/event.util";
1110
import { Collections } from "@backend/common/constants/collections";
11+
import { isBase } from "@backend/event/util/event.util";
1212
import { mockGcalEvents } from "../mocks.gcal/mocks.gcal/factories/gcal.event.factory";
1313

1414
export interface State_AfterGcalImport {
@@ -30,7 +30,7 @@ export interface State_AfterGcalImport {
3030
*/
3131
export const simulateDbAfterGcalImport = async (
3232
db: Db,
33-
userId?: string,
33+
userId: string,
3434
): Promise<State_AfterGcalImport> => {
3535
const { gcalEvents, compassEvents } = mockGcalAndCompassEvents(userId);
3636
await db

packages/backend/src/__tests__/mocks.ccal/ccal.event.factory.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,39 @@
1+
import dayjs from "dayjs";
12
import { ObjectId } from "mongodb";
23
import { faker } from "@faker-js/faker/.";
34
import { Origin, Priorities } from "@core/constants/core.constants";
45
import {
6+
Schema_Event,
57
Schema_Event_Recur_Base,
68
Schema_Event_Recur_Instance,
9+
WithCompassId,
710
} from "@core/types/event.types";
811

12+
export const createMockStandaloneEvent = (
13+
overrides: Partial<Schema_Event> = {},
14+
): WithCompassId<Schema_Event> => {
15+
const start = faker.date.future();
16+
const end = dayjs(start).add(1, "hour");
17+
return {
18+
_id: new ObjectId().toString(),
19+
title: faker.lorem.sentence(),
20+
startDate: start.toISOString(),
21+
endDate: end.toISOString(),
22+
user: "test-user-id",
23+
origin: Origin.COMPASS,
24+
priority: Priorities.WORK,
25+
...overrides,
26+
};
27+
};
28+
929
/**
1030
* Creates a base recurring event with default values that can be overridden.
1131
* @param overrides - The overrides for the mock base event.
1232
* @returns A base recurring event.
1333
*/
1434
export const createMockBaseEvent = (
1535
overrides: Partial<Schema_Event_Recur_Base> = {},
16-
): Schema_Event_Recur_Base => {
36+
): WithCompassId<Schema_Event_Recur_Base> => {
1737
const now = new Date();
1838
return {
1939
_id: new ObjectId().toString(),
@@ -43,7 +63,7 @@ export const createMockBaseEvent = (
4363
export const createMockInstance = (
4464
baseEventId: string,
4565
overrides: Partial<Schema_Event_Recur_Instance> = {},
46-
): Schema_Event_Recur_Instance => {
66+
): WithCompassId<Schema_Event_Recur_Instance> => {
4767
const now = new Date();
4868
return {
4969
_id: new ObjectId().toString(),
@@ -76,8 +96,8 @@ export const createMockInstances = (
7696
baseEvent: Schema_Event_Recur_Base,
7797
count: number,
7898
overrides: Partial<Schema_Event_Recur_Instance> = {},
79-
): Schema_Event_Recur_Instance[] => {
80-
const instances: Schema_Event_Recur_Instance[] = [];
99+
): WithCompassId<Schema_Event_Recur_Instance>[] => {
100+
const instances: WithCompassId<Schema_Event_Recur_Instance>[] = [];
81101
const baseDate = new Date(baseEvent.startDate || "2024-03-20T10:00:00Z");
82102

83103
for (let i = 0; i < count; i++) {

packages/backend/src/__tests__/mocks.gcal/factories/gcal.event.factory.set.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
import { gSchema$EventBase } from "@core/types/gcal";
22
import {
3-
mockRecurringBaseEvent,
4-
mockRecurringInstances,
3+
mockRecurringGcalBaseEvent,
4+
mockRecurringGcalInstances,
55
mockRegularGcalEvent,
66
} from "./gcal.event.factory";
77

88
/* Sets of events, pre-organized as a convenience for testing */
99

1010
export const mockGcalEvents = () => {
1111
// Create a base recurring event
12-
const baseRecurringEvent = mockRecurringBaseEvent({
12+
const baseRecurringEvent = mockRecurringGcalBaseEvent({
1313
id: "recurring-1",
1414
summary: "Recurrence",
1515
recurrence: ["RRULE:FREQ=WEEKLY"],
1616
}) as gSchema$EventBase;
1717

1818
// Create instances of the recurring event
19-
const instances = mockRecurringInstances(baseRecurringEvent, 2, 7);
19+
const instances = mockRecurringGcalInstances(baseRecurringEvent, 2, 7);
2020

2121
// Create a regular event
2222
const regularEvent = mockRegularGcalEvent({

packages/backend/src/__tests__/mocks.gcal/factories/gcal.event.factory.test.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,40 @@
11
import { gSchema$EventInstance } from "@core/types/gcal";
22
import {
3-
mockRecurringBaseEvent,
4-
mockRecurringInstances,
3+
mockRecurringGcalBaseEvent,
4+
mockRecurringGcalInstances,
55
} from "./gcal.event.factory";
66

77
describe("mockRecurringInstances", () => {
88
it("should not include 'recurrence'", () => {
9-
const base = mockRecurringBaseEvent();
10-
const instances = mockRecurringInstances(base, 2, 7);
9+
const base = mockRecurringGcalBaseEvent();
10+
const instances = mockRecurringGcalInstances(base, 2, 7);
1111
instances.forEach((instance) => {
1212
expect(instance).not.toHaveProperty("recurrence");
1313
});
1414
});
1515
it("should create the correct number of instances", () => {
16-
const base = mockRecurringBaseEvent();
17-
const instances = mockRecurringInstances(base, 2, 7);
16+
const base = mockRecurringGcalBaseEvent();
17+
const instances = mockRecurringGcalInstances(base, 2, 7);
1818
expect(instances).toHaveLength(2);
1919
});
2020
it("should include 'recurringEventId' that points to the base event", () => {
21-
const base = mockRecurringBaseEvent();
22-
const instances = mockRecurringInstances(base, 2, 7);
21+
const base = mockRecurringGcalBaseEvent();
22+
const instances = mockRecurringGcalInstances(base, 2, 7);
2323
instances.forEach((instance) => {
2424
expect(instance.recurringEventId).toBe(base.id);
2525
});
2626
});
2727
it("should make the first instance start and end at the same time as the base event", () => {
28-
const base = mockRecurringBaseEvent();
29-
const instances = mockRecurringInstances(base, 2, 7);
28+
const base = mockRecurringGcalBaseEvent();
29+
const instances = mockRecurringGcalInstances(base, 2, 7);
3030
const firstInstance = instances[0] as gSchema$EventInstance;
3131

3232
expect(firstInstance.start?.dateTime).toBe(base.start?.dateTime);
3333
expect(firstInstance.end?.dateTime).toBe(base.end?.dateTime);
3434
});
3535
it("should make instances start and end in the future from the base time (except for the first one)", () => {
36-
const base = mockRecurringBaseEvent();
37-
const instances = mockRecurringInstances(base, 2, 7);
36+
const base = mockRecurringGcalBaseEvent();
37+
const instances = mockRecurringGcalInstances(base, 2, 7);
3838
const baseStart = new Date(base.start?.dateTime as string);
3939
instances.forEach((instance, index) => {
4040
if (index === 0) return; // first instance is the same as the base

packages/backend/src/__tests__/mocks.gcal/factories/gcal.event.factory.ts

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,38 +7,34 @@ import {
77
gSchema$EventInstance,
88
} from "@core/types/gcal";
99

10-
export const mockRegularEvent = (): WithGcalId<gSchema$Event> => ({
11-
id: faker.string.nanoid(),
12-
summary: faker.lorem.sentence(),
13-
start: { dateTime: faker.date.future().toISOString() },
14-
end: { dateTime: faker.date.future().toISOString() },
15-
status: "confirmed",
16-
});
17-
1810
/**
1911
* Returns a base event and its instances
2012
* @param count - The number of instances to create
2113
* @param repeatIntervalInDays - The interval between instances
2214
* @returns An array containing the base event and its instances
2315
*/
24-
export const mockRecurringEvents = (
16+
export const mockRecurringGcalEvents = (
2517
count: number,
2618
repeatIntervalInDays: number,
2719
): (gSchema$EventBase | gSchema$EventInstance)[] => {
28-
const base = mockRecurringBaseEvent();
29-
const instances = mockRecurringInstances(base, count, repeatIntervalInDays);
20+
const base = mockRecurringGcalBaseEvent();
21+
const instances = mockRecurringGcalInstances(
22+
base,
23+
count,
24+
repeatIntervalInDays,
25+
);
3026
return [base, ...instances];
3127
};
3228

33-
export const mockRecurringBaseEvent = (
29+
export const mockRecurringGcalBaseEvent = (
3430
overrides: Partial<gSchema$EventBase> = {},
3531
): gSchema$EventBase => ({
36-
...mockRegularEvent(),
32+
...mockRegularGcalEvent(),
3733
recurrence: ["RRULE:FREQ=WEEKLY"],
3834
...overrides,
3935
});
4036

41-
export const mockRecurringInstances = (
37+
export const mockRecurringGcalInstances = (
4238
base: gSchema$EventBase,
4339
count: number,
4440
repeatIntervalInDays: number,
@@ -90,9 +86,10 @@ export const mockRecurringInstances = (
9086
};
9187

9288
export const mockRegularGcalEvent = (
93-
overrides: Partial<gSchema$Event> = {},
94-
): gSchema$Event => {
95-
const id = faker.string.uuid();
89+
overrides: Partial<WithGcalId<gSchema$Event>> = {},
90+
): WithGcalId<gSchema$Event> => {
91+
const id = faker.string.nanoid();
92+
const tz = faker.location.timeZone();
9693
return {
9794
id,
9895
summary: faker.lorem.sentence(),
@@ -102,11 +99,11 @@ export const mockRegularGcalEvent = (
10299
updated: faker.date.recent().toISOString(),
103100
start: {
104101
dateTime: faker.date.future().toISOString(),
105-
timeZone: "America/Chicago",
102+
timeZone: tz,
106103
},
107104
end: {
108105
dateTime: faker.date.future().toISOString(),
109-
timeZone: "America/Chicago",
106+
timeZone: tz,
110107
},
111108
iCalUID: faker.string.uuid() + "@google.com",
112109
sequence: 0,

packages/backend/src/event/services/event.find.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
import { Collection, Db, MongoClient } from "mongodb";
22
import { MapEvent } from "@core/mappers/map.event";
33
import { Schema_Event } from "@core/types/event.types";
4-
import { isBase, isExistingInstance } from "@core/util/event.util";
54
import {
6-
mockRecurringBaseEvent,
7-
mockRecurringInstances,
5+
mockRecurringGcalBaseEvent,
6+
mockRecurringGcalInstances,
87
} from "@backend/__tests__/mocks.gcal/factories/gcal.event.factory";
98
import { mockEventSetJan22 } from "../../../../core/src/__mocks__/events/events.22jan";
109
import { mockEventSetSomeday1 } from "../../../../core/src/__mocks__/events/events.someday.1";
10+
import { isBase, isExistingInstance } from "../util/event.util";
1111
import { getReadAllFilter } from "./event.service.util";
1212

13-
const gBase = mockRecurringBaseEvent();
14-
const gInstances = mockRecurringInstances(gBase, 10, 7);
13+
const gBase = mockRecurringGcalBaseEvent();
14+
const gInstances = mockRecurringGcalInstances(gBase, 10, 7);
1515

1616
const base = MapEvent.toCompass("user1", [gBase])[0];
1717
const instances = MapEvent.toCompass("user1", gInstances);
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* Event DB operations repo for Compass's Event collection
3+
*/
4+
import { ObjectId } from "mongodb";
5+
import { Event_Core } from "@core/types/event.types";
6+
import { Collections } from "@backend/common/constants/collections";
7+
import mongoService from "@backend/common/services/mongo.service";
8+
import { Ids_Event } from "@backend/event/queries/event.queries";
9+
10+
export class EventRepository {
11+
constructor(private userId: string) {}
12+
13+
/**
14+
* Update an event, creating it if it doesn't exist
15+
*/
16+
async updateById(key: Ids_Event, event: Event_Core) {
17+
const _id = event?._id ? new ObjectId(event._id) : new ObjectId();
18+
if (!event?._id) {
19+
console.log("upserting event with new id:", _id);
20+
} else {
21+
console.log("upserting event with existing id:", event._id);
22+
}
23+
const result = await mongoService.db
24+
.collection(Collections.EVENT)
25+
.updateOne(
26+
{ [key]: event[key], user: this.userId },
27+
{ $set: event },
28+
{ upsert: true },
29+
);
30+
return result;
31+
}
32+
}

packages/backend/src/event/services/recur/repo/recur.event.repo.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ObjectId } from "mongodb";
2+
import { Event_Core } from "@core/types/event.types";
23
import { Collections } from "@backend/common/constants/collections";
34
import mongoService from "@backend/common/services/mongo.service";
45
import { Ids_Event } from "../../../queries/event.queries";
@@ -50,4 +51,21 @@ export class RecurringEventRepository {
5051
});
5152
return result;
5253
}
54+
55+
/**
56+
* Update an instance by its gEventId
57+
* @param updatedInstance - The updated instance
58+
* @returns The result of the update operation
59+
*/
60+
async updateInstance(updatedInstance: Event_Core) {
61+
const gEventId = updatedInstance.gEventId;
62+
63+
const result = await mongoService.db
64+
.collection(Collections.EVENT)
65+
.updateOne(
66+
{ gEventId: gEventId, user: this.userId },
67+
{ $set: updatedInstance },
68+
);
69+
return result;
70+
}
5371
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import {
2+
createMockInstance,
3+
createMockStandaloneEvent,
4+
} from "@backend/__tests__/mocks.ccal/ccal.event.factory";
5+
import { createMockBaseEvent } from "@backend/__tests__/mocks.ccal/ccal.event.factory";
6+
import { categorizeEvents } from "./event.util";
7+
8+
describe("categorizeEvents", () => {
9+
it("should categorize events correctly", () => {
10+
const standalone = createMockStandaloneEvent();
11+
const base = createMockBaseEvent();
12+
const instance = createMockInstance(base._id);
13+
const events = [base, instance, standalone];
14+
15+
const { baseEvents, instances, regularEvents } = categorizeEvents(events);
16+
17+
expect(baseEvents).toEqual([base]);
18+
expect(instances).toEqual([instance]);
19+
expect(regularEvents).toEqual([standalone]);
20+
});
21+
});

packages/core/src/util/event.util.ts renamed to packages/backend/src/event/util/event.util.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ import {
66
} from "@core/types/event.types";
77
import { gSchema$Event } from "@core/types/gcal";
88

9+
export const categorizeEvents = (events: Schema_Event[]) => {
10+
const baseEvents = events.filter(isBase);
11+
const instances = events.filter(isExistingInstance);
12+
const regularEvents = events.filter(
13+
(e) => !isBase(e) && !isExistingInstance(e),
14+
);
15+
return { baseEvents, instances, regularEvents };
16+
};
17+
918
export const categorizeRecurringEvents = (events: Recurrence[]) => {
1019
const baseEvent = events.find(isBase) as Schema_Event_Recur_Base;
1120
const instances = events.filter(

0 commit comments

Comments
 (0)