@kubun/connector-google-calendar 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/actions.js +35 -6
- package/lib/api.d.ts +14 -1
- package/lib/api.js +60 -5
- package/lib/model.d.ts +4 -1
- package/lib/model.js +6 -1
- package/lib/provider.js +12 -7
- package/package.json +9 -9
package/lib/actions.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createEvent, updateEvent } from './api.js';
|
|
1
|
+
import { createEvent, getCalendar, updateEvent } from './api.js';
|
|
2
2
|
export function createGoogleCalendarActionHandler(config) {
|
|
3
3
|
return {
|
|
4
4
|
async create (_model, params) {
|
|
@@ -6,22 +6,51 @@ export function createGoogleCalendarActionHandler(config) {
|
|
|
6
6
|
if (calendarID == null) {
|
|
7
7
|
throw new Error('calendarID is required for creating calendar events');
|
|
8
8
|
}
|
|
9
|
+
// 'primary' is a per-account alias; sync sees the real id. Resolve it once
|
|
10
|
+
// on create so the composite sourceID matches the synced event's identity.
|
|
11
|
+
const resolvedCalendarID = calendarID === 'primary' ? (await getCalendar({
|
|
12
|
+
accessToken: config.credential.accessToken,
|
|
13
|
+
calendarID: 'primary',
|
|
14
|
+
fetch: config.fetch
|
|
15
|
+
})).id : calendarID;
|
|
9
16
|
return createEvent({
|
|
10
17
|
accessToken: config.credential.accessToken,
|
|
11
|
-
calendarID,
|
|
18
|
+
calendarID: resolvedCalendarID,
|
|
12
19
|
params,
|
|
13
20
|
fetch: config.fetch
|
|
14
21
|
});
|
|
15
22
|
},
|
|
16
23
|
async update (_model, sourceID, params) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
// sourceID is the composite calendarID/eventID. On a lookup hit the merged
|
|
25
|
+
// document supplies eventID + calendarID directly; on a miss recover both
|
|
26
|
+
// from the composite. An eventID never contains '/', a calendarID may, so
|
|
27
|
+
// split on the last '/'.
|
|
28
|
+
const mergedEventID = params.eventID;
|
|
29
|
+
const mergedCalendarID = params.calendarID;
|
|
30
|
+
let eventID;
|
|
31
|
+
let calendarID;
|
|
32
|
+
if (mergedEventID != null) {
|
|
33
|
+
if (mergedCalendarID == null) {
|
|
34
|
+
throw new Error('calendarID is required for updating calendar events');
|
|
35
|
+
}
|
|
36
|
+
eventID = mergedEventID;
|
|
37
|
+
calendarID = mergedCalendarID;
|
|
38
|
+
} else {
|
|
39
|
+
const slash = sourceID.lastIndexOf('/');
|
|
40
|
+
if (slash <= 0 || slash === sourceID.length - 1) {
|
|
41
|
+
throw new Error(`Cannot recover eventID from sourceID "${sourceID}"`);
|
|
42
|
+
}
|
|
43
|
+
const recoveredCalendarID = sourceID.slice(0, slash);
|
|
44
|
+
if (mergedCalendarID != null && mergedCalendarID !== recoveredCalendarID) {
|
|
45
|
+
throw new Error('calendarID does not match the event sourceID');
|
|
46
|
+
}
|
|
47
|
+
eventID = sourceID.slice(slash + 1);
|
|
48
|
+
calendarID = recoveredCalendarID;
|
|
20
49
|
}
|
|
21
50
|
return updateEvent({
|
|
22
51
|
accessToken: config.credential.accessToken,
|
|
23
52
|
calendarID,
|
|
24
|
-
eventID
|
|
53
|
+
eventID,
|
|
25
54
|
params,
|
|
26
55
|
fetch: config.fetch
|
|
27
56
|
});
|
package/lib/api.d.ts
CHANGED
|
@@ -49,7 +49,20 @@ export declare function buildEventsListURL(params: {
|
|
|
49
49
|
}): string;
|
|
50
50
|
export declare function mapCalendarToEntity(calendar: GoogleCalendarResource): EntityRecord;
|
|
51
51
|
export declare function mapEventToEntity(event: GoogleEventResource, calendarID: string): EntityRecord;
|
|
52
|
-
export declare function buildEventFromParams(params: Record<string, unknown
|
|
52
|
+
export declare function buildEventFromParams(params: Record<string, unknown>, options?: {
|
|
53
|
+
create?: boolean;
|
|
54
|
+
}): Record<string, unknown>;
|
|
55
|
+
export declare function getCalendar(config: {
|
|
56
|
+
accessToken: string;
|
|
57
|
+
calendarID: string;
|
|
58
|
+
fetch?: typeof globalThis.fetch;
|
|
59
|
+
}): Promise<GoogleCalendarResource>;
|
|
60
|
+
export declare function getEvent(config: {
|
|
61
|
+
accessToken: string;
|
|
62
|
+
calendarID: string;
|
|
63
|
+
eventID: string;
|
|
64
|
+
fetch?: typeof globalThis.fetch;
|
|
65
|
+
}): Promise<EntityRecord>;
|
|
53
66
|
export declare function createEvent(config: {
|
|
54
67
|
accessToken: string;
|
|
55
68
|
calendarID: string;
|
package/lib/api.js
CHANGED
|
@@ -61,7 +61,11 @@ export function mapEventToEntity(event, calendarID) {
|
|
|
61
61
|
const endAt = allDay ? allDayDateToDateTime(event.end.date) : event.end.dateTime;
|
|
62
62
|
return {
|
|
63
63
|
sourceService: 'google-calendar',
|
|
64
|
-
|
|
64
|
+
// Google event ids are unique per calendar, not per account; namespace the
|
|
65
|
+
// identity by calendar so the same id in two calendars stays two documents.
|
|
66
|
+
// The bare id lives on in eventID for PATCH targeting.
|
|
67
|
+
sourceID: `${calendarID}/${event.id}`,
|
|
68
|
+
eventID: event.id,
|
|
65
69
|
sourceURL: event.htmlLink,
|
|
66
70
|
title: event.summary ?? '(No title)',
|
|
67
71
|
description: event.description,
|
|
@@ -80,9 +84,15 @@ export function mapEventToEntity(event, calendarID) {
|
|
|
80
84
|
}
|
|
81
85
|
};
|
|
82
86
|
}
|
|
83
|
-
export function buildEventFromParams(params) {
|
|
87
|
+
export function buildEventFromParams(params, options) {
|
|
84
88
|
const event = {};
|
|
85
89
|
const allDay = params.allDay === true;
|
|
90
|
+
// A caller-supplied id makes a create idempotent (Google rejects a duplicate
|
|
91
|
+
// with 409). Only insert accepts an id; PATCH must never carry one, so gate it
|
|
92
|
+
// to the create path — the builder is shared with updateEvent.
|
|
93
|
+
if (options?.create === true && params.id != null) {
|
|
94
|
+
event.id = params.id;
|
|
95
|
+
}
|
|
86
96
|
if (params.title != null) {
|
|
87
97
|
event.summary = params.title;
|
|
88
98
|
}
|
|
@@ -93,8 +103,11 @@ export function buildEventFromParams(params) {
|
|
|
93
103
|
event.location = params.location;
|
|
94
104
|
}
|
|
95
105
|
if (params.startAt != null) {
|
|
106
|
+
// An imported all-day event carries startAt as a midnight-UTC timestamp
|
|
107
|
+
// (mapEventToEntity anchors bare dates for the date-time schema); slice back
|
|
108
|
+
// to YYYY-MM-DD, which Google requires for start.date/end.date.
|
|
96
109
|
event.start = allDay ? {
|
|
97
|
-
date: params.startAt
|
|
110
|
+
date: params.startAt.slice(0, 10)
|
|
98
111
|
} : {
|
|
99
112
|
dateTime: params.startAt,
|
|
100
113
|
...params.timeZone != null ? {
|
|
@@ -104,7 +117,7 @@ export function buildEventFromParams(params) {
|
|
|
104
117
|
}
|
|
105
118
|
if (params.endAt != null) {
|
|
106
119
|
event.end = allDay ? {
|
|
107
|
-
date: params.endAt
|
|
120
|
+
date: params.endAt.slice(0, 10)
|
|
108
121
|
} : {
|
|
109
122
|
dateTime: params.endAt,
|
|
110
123
|
...params.timeZone != null ? {
|
|
@@ -114,9 +127,40 @@ export function buildEventFromParams(params) {
|
|
|
114
127
|
}
|
|
115
128
|
return event;
|
|
116
129
|
}
|
|
130
|
+
export async function getCalendar(config) {
|
|
131
|
+
const fetchFn = config.fetch ?? globalThis.fetch;
|
|
132
|
+
const url = `${CALENDAR_BASE_URL}/calendars/${encodeURIComponent(config.calendarID)}`;
|
|
133
|
+
const response = await fetchFn(url, {
|
|
134
|
+
headers: {
|
|
135
|
+
Authorization: `Bearer ${config.accessToken}`
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
if (!response.ok) {
|
|
139
|
+
const errorMessage = await parseGoogleError(response);
|
|
140
|
+
throw new Error(`Google Calendar API error: ${errorMessage}`);
|
|
141
|
+
}
|
|
142
|
+
return await response.json();
|
|
143
|
+
}
|
|
144
|
+
export async function getEvent(config) {
|
|
145
|
+
const fetchFn = config.fetch ?? globalThis.fetch;
|
|
146
|
+
const url = `${CALENDAR_BASE_URL}/calendars/${encodeURIComponent(config.calendarID)}/events/${encodeURIComponent(config.eventID)}`;
|
|
147
|
+
const response = await fetchFn(url, {
|
|
148
|
+
headers: {
|
|
149
|
+
Authorization: `Bearer ${config.accessToken}`
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
if (!response.ok) {
|
|
153
|
+
const errorMessage = await parseGoogleError(response);
|
|
154
|
+
throw new Error(`Google Calendar API error: ${errorMessage}`);
|
|
155
|
+
}
|
|
156
|
+
const data = await response.json();
|
|
157
|
+
return mapEventToEntity(data, config.calendarID);
|
|
158
|
+
}
|
|
117
159
|
export async function createEvent(config) {
|
|
118
160
|
const fetchFn = config.fetch ?? globalThis.fetch;
|
|
119
|
-
const event = buildEventFromParams(config.params
|
|
161
|
+
const event = buildEventFromParams(config.params, {
|
|
162
|
+
create: true
|
|
163
|
+
});
|
|
120
164
|
const url = `${CALENDAR_BASE_URL}/calendars/${encodeURIComponent(config.calendarID)}/events`;
|
|
121
165
|
const response = await fetchFn(url, {
|
|
122
166
|
method: 'POST',
|
|
@@ -127,6 +171,17 @@ export async function createEvent(config) {
|
|
|
127
171
|
body: JSON.stringify(event)
|
|
128
172
|
});
|
|
129
173
|
if (!response.ok) {
|
|
174
|
+
// A 409 on a caller-supplied id means the event already exists — the create
|
|
175
|
+
// is already committed, so a retry after a lost response is idempotent: fetch
|
|
176
|
+
// and return the existing event instead of failing.
|
|
177
|
+
if (response.status === 409 && config.params.id != null) {
|
|
178
|
+
return getEvent({
|
|
179
|
+
accessToken: config.accessToken,
|
|
180
|
+
calendarID: config.calendarID,
|
|
181
|
+
eventID: config.params.id,
|
|
182
|
+
fetch: config.fetch
|
|
183
|
+
});
|
|
184
|
+
}
|
|
130
185
|
const errorMessage = await parseGoogleError(response);
|
|
131
186
|
throw new Error(`Google Calendar API error: ${errorMessage}`);
|
|
132
187
|
}
|
package/lib/model.d.ts
CHANGED
|
@@ -98,6 +98,9 @@ export declare const googleCalendarEventDefinition: {
|
|
|
98
98
|
readonly recurringEventID: {
|
|
99
99
|
readonly type: "string";
|
|
100
100
|
};
|
|
101
|
+
readonly eventID: {
|
|
102
|
+
readonly type: "string";
|
|
103
|
+
};
|
|
101
104
|
readonly calendarID: {
|
|
102
105
|
readonly type: "string";
|
|
103
106
|
};
|
|
@@ -130,7 +133,7 @@ export declare const googleCalendarEventDefinition: {
|
|
|
130
133
|
readonly type: "string";
|
|
131
134
|
readonly title: "SourceID";
|
|
132
135
|
readonly minLength: 1;
|
|
133
|
-
readonly maxLength:
|
|
136
|
+
readonly maxLength: 1400;
|
|
134
137
|
};
|
|
135
138
|
readonly SourceURL: {
|
|
136
139
|
readonly type: "string";
|
package/lib/model.js
CHANGED
|
@@ -108,6 +108,9 @@ export const googleCalendarEventDefinition = {
|
|
|
108
108
|
recurringEventID: {
|
|
109
109
|
type: 'string'
|
|
110
110
|
},
|
|
111
|
+
eventID: {
|
|
112
|
+
type: 'string'
|
|
113
|
+
},
|
|
111
114
|
calendarID: {
|
|
112
115
|
type: 'string'
|
|
113
116
|
},
|
|
@@ -145,11 +148,13 @@ export const googleCalendarEventDefinition = {
|
|
|
145
148
|
minLength: 1,
|
|
146
149
|
maxLength: 50
|
|
147
150
|
},
|
|
151
|
+
// A composite calendarID/eventID: calendar id (email-like, up to ~254) +
|
|
152
|
+
// '/' + a Google event id (up to 1024). 500 is too small.
|
|
148
153
|
SourceID: {
|
|
149
154
|
type: 'string',
|
|
150
155
|
title: 'SourceID',
|
|
151
156
|
minLength: 1,
|
|
152
|
-
maxLength:
|
|
157
|
+
maxLength: 1400
|
|
153
158
|
},
|
|
154
159
|
SourceURL: urlSchema,
|
|
155
160
|
DateTime: dateTimeSchema,
|
package/lib/provider.js
CHANGED
|
@@ -31,7 +31,9 @@ function computeTimeMin(boundary) {
|
|
|
31
31
|
// Parse PnD format (e.g. "P30D" = 30 days)
|
|
32
32
|
const match = /^P(\d+)D$/i.exec(boundary.maxAge);
|
|
33
33
|
if (match == null) return undefined;
|
|
34
|
-
const
|
|
34
|
+
const rawDays = match[1];
|
|
35
|
+
if (rawDays == null) return undefined;
|
|
36
|
+
const days = Number.parseInt(rawDays, 10);
|
|
35
37
|
const date = new Date();
|
|
36
38
|
date.setDate(date.getDate() - days);
|
|
37
39
|
return date.toISOString();
|
|
@@ -104,8 +106,7 @@ export function createGoogleCalendarProvider(config) {
|
|
|
104
106
|
// Phase 2: fetch events per calendar
|
|
105
107
|
const calendarTokens = {};
|
|
106
108
|
let eventBatchYielded = false;
|
|
107
|
-
for(
|
|
108
|
-
const calendar = allCalendars[i];
|
|
109
|
+
for (const [i, calendar] of allCalendars.entries()){
|
|
109
110
|
const isLastCalendar = i === allCalendars.length - 1;
|
|
110
111
|
let eventsSyncToken;
|
|
111
112
|
for await (const page of fetchEventsForCalendar({
|
|
@@ -135,9 +136,11 @@ export function createGoogleCalendarProvider(config) {
|
|
|
135
136
|
yield {
|
|
136
137
|
modelID: CALENDAR_EVENT_MODEL_ID,
|
|
137
138
|
entities: activeItems.map((event)=>mapEventToEntity(event, calendar.id)),
|
|
139
|
+
// Match the composite identity mapEventToEntity assigns, so the
|
|
140
|
+
// tombstone lookup hits the calendar-namespaced document.
|
|
138
141
|
deleted: cancelledItems.map((e)=>({
|
|
139
142
|
sourceService: 'google-calendar',
|
|
140
|
-
sourceID: e.id
|
|
143
|
+
sourceID: `${calendar.id}/${e.id}`
|
|
141
144
|
})),
|
|
142
145
|
hasMore: !(isLastCalendar && isLastPage),
|
|
143
146
|
checkpoint
|
|
@@ -220,8 +223,8 @@ export function createGoogleCalendarProvider(config) {
|
|
|
220
223
|
}))
|
|
221
224
|
];
|
|
222
225
|
const updatedCalendarTokens = {};
|
|
223
|
-
for(
|
|
224
|
-
const { id: calendarID, isNew } =
|
|
226
|
+
for (const [i, entry] of allCalendarIDsToSync.entries()){
|
|
227
|
+
const { id: calendarID, isNew } = entry;
|
|
225
228
|
const isLastCalendar = i === allCalendarIDsToSync.length - 1;
|
|
226
229
|
// Use stored syncToken for known calendars; use timeMin for new ones
|
|
227
230
|
const storedToken = isNew ? undefined : prevCalendarTokens[calendarID]?.eventsSyncToken;
|
|
@@ -253,9 +256,11 @@ export function createGoogleCalendarProvider(config) {
|
|
|
253
256
|
yield {
|
|
254
257
|
modelID: CALENDAR_EVENT_MODEL_ID,
|
|
255
258
|
entities: activeItems.map((event)=>mapEventToEntity(event, calendarID)),
|
|
259
|
+
// Match the composite identity mapEventToEntity assigns, so the
|
|
260
|
+
// tombstone lookup hits the calendar-namespaced document.
|
|
256
261
|
deleted: cancelledItems.map((e)=>({
|
|
257
262
|
sourceService: 'google-calendar',
|
|
258
|
-
sourceID: e.id
|
|
263
|
+
sourceID: `${calendarID}/${e.id}`
|
|
259
264
|
})),
|
|
260
265
|
hasMore: !(isLastCalendar && isLastPage),
|
|
261
266
|
checkpoint: newCheckpoint
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubun/connector-google-calendar",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"keywords": [],
|
|
5
5
|
"license": "see LICENSE.md",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -15,12 +15,12 @@
|
|
|
15
15
|
"LICENSE.md"
|
|
16
16
|
],
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@kubun/connector": "^0.
|
|
19
|
-
"@kubun/credential-types": "^0.
|
|
20
|
-
"@kubun/models": "^0.
|
|
21
|
-
"@kubun/protocol": "^0.
|
|
22
|
-
"@kubun/provider-google": "^0.
|
|
23
|
-
"@kubun/scalars": "^0.
|
|
18
|
+
"@kubun/connector": "^0.17.0",
|
|
19
|
+
"@kubun/credential-types": "^0.17.0",
|
|
20
|
+
"@kubun/models": "^0.17.0",
|
|
21
|
+
"@kubun/protocol": "^0.17.0",
|
|
22
|
+
"@kubun/provider-google": "^0.17.0",
|
|
23
|
+
"@kubun/scalars": "^0.17.0"
|
|
24
24
|
},
|
|
25
25
|
"publishConfig": {
|
|
26
26
|
"access": "public"
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"scripts": {
|
|
29
29
|
"build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",
|
|
30
30
|
"build:clean": "del lib",
|
|
31
|
-
"build:js": "swc src -d ./lib --config-file ../../node_modules/@kigu/dev/swc.json --strip-leading-paths",
|
|
32
|
-
"build:types": "tsc --emitDeclarationOnly --skipLibCheck",
|
|
31
|
+
"build:js": "del 'lib/**/*.js' && swc src -d ./lib --config-file ../../node_modules/@kigu/dev/swc.json --strip-leading-paths",
|
|
32
|
+
"build:types": "del 'lib/**/*.d.ts' 'lib/**/*.d.ts.map' && tsc --emitDeclarationOnly --skipLibCheck",
|
|
33
33
|
"build:types:ci": "tsc --emitDeclarationOnly --declarationMap false",
|
|
34
34
|
"test": "pnpm run test:types && pnpm run test:unit",
|
|
35
35
|
"test:types": "tsc --noEmit -p tsconfig.test.json",
|