@kubun/connector-google-calendar 0.10.0 → 0.12.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 CHANGED
@@ -1 +1,30 @@
1
- import{createEvent as e,updateEvent as r}from"./api.js";export function createGoogleCalendarActionHandler(a){return{async create(r,n){let c=n.calendarID;if(null==c)throw Error("calendarID is required for creating calendar events");return e({accessToken:a.credential.accessToken,calendarID:c,params:n,fetch:a.fetch})},async update(e,n,c){let t=c.calendarID;if(null==t)throw Error("calendarID is required for updating calendar events");return r({accessToken:a.credential.accessToken,calendarID:t,eventID:n,params:c,fetch:a.fetch})}}}
1
+ import { createEvent, updateEvent } from './api.js';
2
+ export function createGoogleCalendarActionHandler(config) {
3
+ return {
4
+ async create (_model, params) {
5
+ const calendarID = params.calendarID;
6
+ if (calendarID == null) {
7
+ throw new Error('calendarID is required for creating calendar events');
8
+ }
9
+ return createEvent({
10
+ accessToken: config.credential.accessToken,
11
+ calendarID,
12
+ params,
13
+ fetch: config.fetch
14
+ });
15
+ },
16
+ async update (_model, sourceID, params) {
17
+ const calendarID = params.calendarID;
18
+ if (calendarID == null) {
19
+ throw new Error('calendarID is required for updating calendar events');
20
+ }
21
+ return updateEvent({
22
+ accessToken: config.credential.accessToken,
23
+ calendarID,
24
+ eventID: sourceID,
25
+ params,
26
+ fetch: config.fetch
27
+ });
28
+ }
29
+ };
30
+ }
package/lib/api.js CHANGED
@@ -1 +1,142 @@
1
- let e="https://www.googleapis.com/calendar/v3";async function t(e){try{let t=await e.json();return t.error?.message??`${e.status} ${e.statusText}`}catch{return`${e.status} ${e.statusText}`}}export function buildCalendarListURL(t){let n=new URL(`${e}/users/me/calendarList`);return n.searchParams.set("showHidden","false"),null!=t.syncToken&&n.searchParams.set("syncToken",t.syncToken),null!=t.pageToken&&n.searchParams.set("pageToken",t.pageToken),n.toString()}export function buildEventsListURL(t){let n=new URL(`${e}/calendars/${encodeURIComponent(t.calendarID)}/events`);return n.searchParams.set("singleEvents","true"),null!=t.syncToken?n.searchParams.set("syncToken",t.syncToken):null!=t.timeMin&&n.searchParams.set("timeMin",t.timeMin),null!=t.pageToken&&n.searchParams.set("pageToken",t.pageToken),n.toString()}export function mapCalendarToEntity(e){return{sourceService:"google-calendar",sourceID:e.id,sourceURL:"https://calendar.google.com",summary:e.summary,description:e.description,timeZone:e.timeZone,primary:e.primary??!1,backgroundColor:e.backgroundColor}}export function mapEventToEntity(e,t){let n=null!=e.start.date,a=n?e.start.date:e.start.dateTime,r=n?e.end.date:e.end.dateTime;return{sourceService:"google-calendar",sourceID:e.id,sourceURL:e.htmlLink,title:e.summary??"(No title)",description:e.description,location:e.location,allDay:n,startAt:a,endAt:r,status:e.status.toUpperCase(),htmlLink:e.htmlLink,recurringEventID:e.recurringEventID,timeZone:e.start.timeZone,calendarID:t,calendar:{sourceService:"google-calendar",sourceID:t}}}export function buildEventFromParams(e){let t={},n=!0===e.allDay;return null!=e.title&&(t.summary=e.title),null!=e.description&&(t.description=e.description),null!=e.location&&(t.location=e.location),null!=e.startAt&&(t.start=n?{date:e.startAt}:{dateTime:e.startAt,...null!=e.timeZone?{timeZone:e.timeZone}:{}}),null!=e.endAt&&(t.end=n?{date:e.endAt}:{dateTime:e.endAt,...null!=e.timeZone?{timeZone:e.timeZone}:{}}),t}export async function createEvent(n){let a=n.fetch??globalThis.fetch,r=buildEventFromParams(n.params),o=`${e}/calendars/${encodeURIComponent(n.calendarID)}/events`,s=await a(o,{method:"POST",headers:{Authorization:`Bearer ${n.accessToken}`,"Content-Type":"application/json"},body:JSON.stringify(r)});if(!s.ok){let e=await t(s);throw Error(`Google Calendar API error: ${e}`)}return mapEventToEntity(await s.json(),n.calendarID)}export async function updateEvent(n){let a=n.fetch??globalThis.fetch,r=buildEventFromParams(n.params),o=`${e}/calendars/${encodeURIComponent(n.calendarID)}/events/${encodeURIComponent(n.eventID)}`,s=await a(o,{method:"PATCH",headers:{Authorization:`Bearer ${n.accessToken}`,"Content-Type":"application/json"},body:JSON.stringify(r)});if(!s.ok){let e=await t(s);throw Error(`Google Calendar API error: ${e}`)}return mapEventToEntity(await s.json(),n.calendarID)}
1
+ const CALENDAR_BASE_URL = 'https://www.googleapis.com/calendar/v3';
2
+ async function parseGoogleError(response) {
3
+ try {
4
+ const body = await response.json();
5
+ return body.error?.message ?? `${response.status} ${response.statusText}`;
6
+ } catch {
7
+ return `${response.status} ${response.statusText}`;
8
+ }
9
+ }
10
+ export function buildCalendarListURL(params) {
11
+ const url = new URL(`${CALENDAR_BASE_URL}/users/me/calendarList`);
12
+ url.searchParams.set('showHidden', 'false');
13
+ if (params.syncToken != null) {
14
+ url.searchParams.set('syncToken', params.syncToken);
15
+ }
16
+ if (params.pageToken != null) {
17
+ url.searchParams.set('pageToken', params.pageToken);
18
+ }
19
+ return url.toString();
20
+ }
21
+ export function buildEventsListURL(params) {
22
+ const url = new URL(`${CALENDAR_BASE_URL}/calendars/${encodeURIComponent(params.calendarID)}/events`);
23
+ url.searchParams.set('singleEvents', 'true');
24
+ if (params.syncToken != null) {
25
+ url.searchParams.set('syncToken', params.syncToken);
26
+ } else if (params.timeMin != null) {
27
+ url.searchParams.set('timeMin', params.timeMin);
28
+ }
29
+ if (params.pageToken != null) {
30
+ url.searchParams.set('pageToken', params.pageToken);
31
+ }
32
+ return url.toString();
33
+ }
34
+ export function mapCalendarToEntity(calendar) {
35
+ return {
36
+ sourceService: 'google-calendar',
37
+ sourceID: calendar.id,
38
+ sourceURL: 'https://calendar.google.com',
39
+ summary: calendar.summary,
40
+ description: calendar.description,
41
+ timeZone: calendar.timeZone,
42
+ primary: calendar.primary ?? false,
43
+ backgroundColor: calendar.backgroundColor
44
+ };
45
+ }
46
+ export function mapEventToEntity(event, calendarID) {
47
+ const allDay = event.start.date != null;
48
+ const startAt = allDay ? event.start.date : event.start.dateTime;
49
+ const endAt = allDay ? event.end.date : event.end.dateTime;
50
+ return {
51
+ sourceService: 'google-calendar',
52
+ sourceID: event.id,
53
+ sourceURL: event.htmlLink,
54
+ title: event.summary ?? '(No title)',
55
+ description: event.description,
56
+ location: event.location,
57
+ allDay,
58
+ startAt,
59
+ endAt,
60
+ status: event.status.toUpperCase(),
61
+ htmlLink: event.htmlLink,
62
+ recurringEventID: event.recurringEventID,
63
+ timeZone: event.start.timeZone,
64
+ calendarID,
65
+ calendar: {
66
+ sourceService: 'google-calendar',
67
+ sourceID: calendarID
68
+ }
69
+ };
70
+ }
71
+ export function buildEventFromParams(params) {
72
+ const event = {};
73
+ const allDay = params.allDay === true;
74
+ if (params.title != null) {
75
+ event.summary = params.title;
76
+ }
77
+ if (params.description != null) {
78
+ event.description = params.description;
79
+ }
80
+ if (params.location != null) {
81
+ event.location = params.location;
82
+ }
83
+ if (params.startAt != null) {
84
+ event.start = allDay ? {
85
+ date: params.startAt
86
+ } : {
87
+ dateTime: params.startAt,
88
+ ...params.timeZone != null ? {
89
+ timeZone: params.timeZone
90
+ } : {}
91
+ };
92
+ }
93
+ if (params.endAt != null) {
94
+ event.end = allDay ? {
95
+ date: params.endAt
96
+ } : {
97
+ dateTime: params.endAt,
98
+ ...params.timeZone != null ? {
99
+ timeZone: params.timeZone
100
+ } : {}
101
+ };
102
+ }
103
+ return event;
104
+ }
105
+ export async function createEvent(config) {
106
+ const fetchFn = config.fetch ?? globalThis.fetch;
107
+ const event = buildEventFromParams(config.params);
108
+ const url = `${CALENDAR_BASE_URL}/calendars/${encodeURIComponent(config.calendarID)}/events`;
109
+ const response = await fetchFn(url, {
110
+ method: 'POST',
111
+ headers: {
112
+ Authorization: `Bearer ${config.accessToken}`,
113
+ 'Content-Type': 'application/json'
114
+ },
115
+ body: JSON.stringify(event)
116
+ });
117
+ if (!response.ok) {
118
+ const errorMessage = await parseGoogleError(response);
119
+ throw new Error(`Google Calendar API error: ${errorMessage}`);
120
+ }
121
+ const data = await response.json();
122
+ return mapEventToEntity(data, config.calendarID);
123
+ }
124
+ export async function updateEvent(config) {
125
+ const fetchFn = config.fetch ?? globalThis.fetch;
126
+ const event = buildEventFromParams(config.params);
127
+ const url = `${CALENDAR_BASE_URL}/calendars/${encodeURIComponent(config.calendarID)}/events/${encodeURIComponent(config.eventID)}`;
128
+ const response = await fetchFn(url, {
129
+ method: 'PATCH',
130
+ headers: {
131
+ Authorization: `Bearer ${config.accessToken}`,
132
+ 'Content-Type': 'application/json'
133
+ },
134
+ body: JSON.stringify(event)
135
+ });
136
+ if (!response.ok) {
137
+ const errorMessage = await parseGoogleError(response);
138
+ throw new Error(`Google Calendar API error: ${errorMessage}`);
139
+ }
140
+ const data = await response.json();
141
+ return mapEventToEntity(data, config.calendarID);
142
+ }
package/lib/connector.js CHANGED
@@ -1 +1,105 @@
1
- import{createGoogleCalendarActionHandler as e}from"./actions.js";import{googleCalendarClusters as t}from"./model.js";import{createGoogleCalendarProvider as r}from"./provider.js";export function createGoogleCalendarConnector(n){return{name:"google-calendar",version:"1.0.0",clusters:t,syncStrategies:["on-demand"],actions:[{name:"create",description:"Create a Google Calendar event",input:{type:"object",properties:{calendarID:{type:"string"},title:{type:"string"},startAt:{type:"string"},endAt:{type:"string"},allDay:{type:"boolean"},description:{type:"string"},location:{type:"string"},timeZone:{type:"string"}}},requiresCredential:!0},{name:"update",description:"Update a Google Calendar event",input:{type:"object",properties:{calendarID:{type:"string"},title:{type:"string"},startAt:{type:"string"},endAt:{type:"string"},allDay:{type:"boolean"},description:{type:"string"},location:{type:"string"},timeZone:{type:"string"}}},requiresCredential:!0}],actionHandler:t=>e({credential:t.credential,fetch:n?.fetch}),auth:{provider:"google",scopes:["https://www.googleapis.com/auth/calendar.readonly"],writeScopes:["https://www.googleapis.com/auth/calendar"]},serverProvider:e=>r({credential:e.credential,boundary:e.boundary,fetch:n?.fetch})}}
1
+ import { createGoogleCalendarActionHandler } from './actions.js';
2
+ import { googleCalendarClusters } from './model.js';
3
+ import { createGoogleCalendarProvider } from './provider.js';
4
+ export function createGoogleCalendarConnector(options) {
5
+ return {
6
+ name: 'google-calendar',
7
+ version: '1.0.0',
8
+ clusters: googleCalendarClusters,
9
+ syncStrategies: [
10
+ 'on-demand'
11
+ ],
12
+ actions: [
13
+ {
14
+ name: 'create',
15
+ description: 'Create a Google Calendar event',
16
+ input: {
17
+ type: 'object',
18
+ properties: {
19
+ calendarID: {
20
+ type: 'string'
21
+ },
22
+ title: {
23
+ type: 'string'
24
+ },
25
+ startAt: {
26
+ type: 'string'
27
+ },
28
+ endAt: {
29
+ type: 'string'
30
+ },
31
+ allDay: {
32
+ type: 'boolean'
33
+ },
34
+ description: {
35
+ type: 'string'
36
+ },
37
+ location: {
38
+ type: 'string'
39
+ },
40
+ timeZone: {
41
+ type: 'string'
42
+ }
43
+ }
44
+ },
45
+ requiresCredential: true
46
+ },
47
+ {
48
+ name: 'update',
49
+ description: 'Update a Google Calendar event',
50
+ input: {
51
+ type: 'object',
52
+ properties: {
53
+ calendarID: {
54
+ type: 'string'
55
+ },
56
+ title: {
57
+ type: 'string'
58
+ },
59
+ startAt: {
60
+ type: 'string'
61
+ },
62
+ endAt: {
63
+ type: 'string'
64
+ },
65
+ allDay: {
66
+ type: 'boolean'
67
+ },
68
+ description: {
69
+ type: 'string'
70
+ },
71
+ location: {
72
+ type: 'string'
73
+ },
74
+ timeZone: {
75
+ type: 'string'
76
+ }
77
+ }
78
+ },
79
+ requiresCredential: true
80
+ }
81
+ ],
82
+ actionHandler: (config)=>{
83
+ return createGoogleCalendarActionHandler({
84
+ credential: config.credential,
85
+ fetch: options?.fetch
86
+ });
87
+ },
88
+ auth: {
89
+ provider: 'google',
90
+ scopes: [
91
+ 'https://www.googleapis.com/auth/calendar.readonly'
92
+ ],
93
+ writeScopes: [
94
+ 'https://www.googleapis.com/auth/calendar'
95
+ ]
96
+ },
97
+ serverProvider: (config)=>{
98
+ return createGoogleCalendarProvider({
99
+ credential: config.credential,
100
+ boundary: config.boundary,
101
+ fetch: options?.fetch
102
+ });
103
+ }
104
+ };
105
+ }
package/lib/index.js CHANGED
@@ -1 +1,5 @@
1
- export{createGoogleCalendarActionHandler}from"./actions.js";export{buildCalendarListURL,buildEventFromParams,buildEventsListURL,createEvent,mapCalendarToEntity,mapEventToEntity,updateEvent}from"./api.js";export{createGoogleCalendarConnector}from"./connector.js";export{googleCalendarClusters,googleCalendarDefinition,googleCalendarEventDefinition}from"./model.js";export{createGoogleCalendarProvider}from"./provider.js";
1
+ export { createGoogleCalendarActionHandler } from './actions.js';
2
+ export { buildCalendarListURL, buildEventFromParams, buildEventsListURL, createEvent, mapCalendarToEntity, mapEventToEntity, updateEvent } from './api.js';
3
+ export { createGoogleCalendarConnector } from './connector.js';
4
+ export { googleCalendarClusters, googleCalendarDefinition, googleCalendarEventDefinition } from './model.js';
5
+ export { createGoogleCalendarProvider } from './provider.js';
package/lib/model.js CHANGED
@@ -1 +1,185 @@
1
- import{connectorClusters as e,connectorModelIDs as t}from"@kubun/connector";import{modelIDs as i,modelsClusters as n}from"@kubun/models";import{GraphBuilder as r}from"@kubun/protocol";import{dateTimeSchema as o,docIDSchema as a,timeZoneSchema as s,urlSchema as c}from"@kubun/scalars";export const googleCalendarDefinition={name:"GoogleCalendar",behavior:"default",interfaces:[t.ExternalEntity],schema:{type:"object",properties:{summary:{type:"string"},description:{type:"string"},timeZone:{$ref:"#/definitions/TimeZone"},primary:{type:"boolean"},backgroundColor:{type:"string"},sourceService:{$ref:"#/definitions/SourceService"},sourceID:{$ref:"#/definitions/SourceID"},sourceURL:{$ref:"#/definitions/SourceURL"},lastSyncedAt:{$ref:"#/definitions/DateTime"}},required:["summary","sourceService","sourceID","lastSyncedAt"],additionalProperties:!1,definitions:{SourceService:{type:"string",title:"SourceService",minLength:1,maxLength:50},SourceID:{type:"string",title:"SourceID",minLength:1,maxLength:500},SourceURL:c,DateTime:o,TimeZone:s}},fieldsMeta:{}};export const googleCalendarEventDefinition={name:"GoogleCalendarEvent",behavior:"default",interfaces:[t.ExternalEntity,i.EventKind],schema:{type:"object",properties:{title:{type:"string",minLength:1,maxLength:200},startAt:{$ref:"#/definitions/DateTime"},endAt:{$ref:"#/definitions/DateTime"},allDay:{type:"boolean"},timeZone:{$ref:"#/definitions/TimeZone"},description:{type:"string"},location:{type:"string"},status:{$ref:"#/definitions/EventStatus"},htmlLink:{$ref:"#/definitions/HtmlLink"},recurringEventID:{type:"string"},calendarID:{type:"string"},calendar:{$ref:"#/definitions/CalendarDocID"},sourceService:{$ref:"#/definitions/SourceService"},sourceID:{$ref:"#/definitions/SourceID"},sourceURL:{$ref:"#/definitions/SourceURL"},lastSyncedAt:{$ref:"#/definitions/DateTime"}},required:["title","startAt","allDay","calendarID","calendar","sourceService","sourceID","lastSyncedAt"],additionalProperties:!1,definitions:{SourceService:{type:"string",title:"SourceService",minLength:1,maxLength:50},SourceID:{type:"string",title:"SourceID",minLength:1,maxLength:500},SourceURL:c,DateTime:o,TimeZone:s,HtmlLink:c,CalendarDocID:a,EventStatus:{type:"string",title:"EventStatus",enum:["CONFIRMED","TENTATIVE","CANCELLED"]}}},fieldsMeta:{calendar:{type:"document",model:"#0"}}};let l=new r({externalClusters:{...e,...n}});l.add(googleCalendarDefinition),l.add(googleCalendarEventDefinition);export const googleCalendarClusters=l.build();
1
+ import { connectorClusters, connectorModelIDs } from '@kubun/connector';
2
+ import { modelIDs, modelsClusters } from '@kubun/models';
3
+ import { GraphBuilder } from '@kubun/protocol';
4
+ import { dateTimeSchema, docIDSchema, timeZoneSchema, urlSchema } from '@kubun/scalars';
5
+ export const googleCalendarDefinition = {
6
+ name: 'GoogleCalendar',
7
+ behavior: 'default',
8
+ interfaces: [
9
+ connectorModelIDs.ExternalEntity
10
+ ],
11
+ schema: {
12
+ type: 'object',
13
+ properties: {
14
+ summary: {
15
+ type: 'string'
16
+ },
17
+ description: {
18
+ type: 'string'
19
+ },
20
+ timeZone: {
21
+ $ref: '#/definitions/TimeZone'
22
+ },
23
+ primary: {
24
+ type: 'boolean'
25
+ },
26
+ backgroundColor: {
27
+ type: 'string'
28
+ },
29
+ sourceService: {
30
+ $ref: '#/definitions/SourceService'
31
+ },
32
+ sourceID: {
33
+ $ref: '#/definitions/SourceID'
34
+ },
35
+ sourceURL: {
36
+ $ref: '#/definitions/SourceURL'
37
+ },
38
+ lastSyncedAt: {
39
+ $ref: '#/definitions/DateTime'
40
+ }
41
+ },
42
+ required: [
43
+ 'summary',
44
+ 'sourceService',
45
+ 'sourceID',
46
+ 'lastSyncedAt'
47
+ ],
48
+ additionalProperties: false,
49
+ definitions: {
50
+ SourceService: {
51
+ type: 'string',
52
+ title: 'SourceService',
53
+ minLength: 1,
54
+ maxLength: 50
55
+ },
56
+ SourceID: {
57
+ type: 'string',
58
+ title: 'SourceID',
59
+ minLength: 1,
60
+ maxLength: 500
61
+ },
62
+ SourceURL: urlSchema,
63
+ DateTime: dateTimeSchema,
64
+ TimeZone: timeZoneSchema
65
+ }
66
+ },
67
+ fieldsMeta: {}
68
+ };
69
+ export const googleCalendarEventDefinition = {
70
+ name: 'GoogleCalendarEvent',
71
+ behavior: 'default',
72
+ interfaces: [
73
+ connectorModelIDs.ExternalEntity,
74
+ modelIDs.EventKind
75
+ ],
76
+ schema: {
77
+ type: 'object',
78
+ properties: {
79
+ title: {
80
+ type: 'string',
81
+ minLength: 1,
82
+ maxLength: 200
83
+ },
84
+ startAt: {
85
+ $ref: '#/definitions/DateTime'
86
+ },
87
+ endAt: {
88
+ $ref: '#/definitions/DateTime'
89
+ },
90
+ allDay: {
91
+ type: 'boolean'
92
+ },
93
+ timeZone: {
94
+ $ref: '#/definitions/TimeZone'
95
+ },
96
+ description: {
97
+ type: 'string'
98
+ },
99
+ location: {
100
+ type: 'string'
101
+ },
102
+ status: {
103
+ $ref: '#/definitions/EventStatus'
104
+ },
105
+ htmlLink: {
106
+ $ref: '#/definitions/HtmlLink'
107
+ },
108
+ recurringEventID: {
109
+ type: 'string'
110
+ },
111
+ calendarID: {
112
+ type: 'string'
113
+ },
114
+ calendar: {
115
+ $ref: '#/definitions/CalendarDocID'
116
+ },
117
+ sourceService: {
118
+ $ref: '#/definitions/SourceService'
119
+ },
120
+ sourceID: {
121
+ $ref: '#/definitions/SourceID'
122
+ },
123
+ sourceURL: {
124
+ $ref: '#/definitions/SourceURL'
125
+ },
126
+ lastSyncedAt: {
127
+ $ref: '#/definitions/DateTime'
128
+ }
129
+ },
130
+ required: [
131
+ 'title',
132
+ 'startAt',
133
+ 'allDay',
134
+ 'calendarID',
135
+ 'calendar',
136
+ 'sourceService',
137
+ 'sourceID',
138
+ 'lastSyncedAt'
139
+ ],
140
+ additionalProperties: false,
141
+ definitions: {
142
+ SourceService: {
143
+ type: 'string',
144
+ title: 'SourceService',
145
+ minLength: 1,
146
+ maxLength: 50
147
+ },
148
+ SourceID: {
149
+ type: 'string',
150
+ title: 'SourceID',
151
+ minLength: 1,
152
+ maxLength: 500
153
+ },
154
+ SourceURL: urlSchema,
155
+ DateTime: dateTimeSchema,
156
+ TimeZone: timeZoneSchema,
157
+ HtmlLink: urlSchema,
158
+ CalendarDocID: docIDSchema,
159
+ EventStatus: {
160
+ type: 'string',
161
+ title: 'EventStatus',
162
+ enum: [
163
+ 'CONFIRMED',
164
+ 'TENTATIVE',
165
+ 'CANCELLED'
166
+ ]
167
+ }
168
+ }
169
+ },
170
+ fieldsMeta: {
171
+ calendar: {
172
+ type: 'document',
173
+ model: '#0'
174
+ }
175
+ }
176
+ };
177
+ const builder = new GraphBuilder({
178
+ externalClusters: {
179
+ ...connectorClusters,
180
+ ...modelsClusters
181
+ }
182
+ });
183
+ builder.add(googleCalendarDefinition); // #0
184
+ builder.add(googleCalendarEventDefinition); // #1
185
+ export const googleCalendarClusters = builder.build();
package/lib/provider.js CHANGED
@@ -1 +1,282 @@
1
- import{resolveModelID as e}from"@kubun/connector";import{buildCalendarListURL as n,buildEventsListURL as t,mapCalendarToEntity as l,mapEventToEntity as a}from"./api.js";import{googleCalendarClusters as i}from"./model.js";let o=e(i,"GoogleCalendar"),c=e(i,"GoogleCalendarEvent");async function r(e,n,t,l){let a=await t(e,{headers:{Authorization:`Bearer ${n.accessToken}`},signal:l});if(!a.ok)throw Error(`Google Calendar API error: ${a.status} ${a.statusText}`);return await a.json()}function s(e){if(null==e.maxAge)return;let n=/^P(\d+)D$/i.exec(e.maxAge);if(null==n)return;let t=Number.parseInt(n[1],10),l=new Date;return l.setDate(l.getDate()-t),l.toISOString()}async function*d(e){let t,l=!0;do{let a=n({syncToken:l?e.syncToken:void 0,pageToken:t}),i=await r(a,e.credential,e.fetchFn,e.signal);yield{items:i.items??[],nextSyncToken:i.nextSyncToken,isFirstPage:l},t=i.nextPageToken,l=!1}while(null!=t)}async function*u(e){let n,l=!0;do{let a=t({calendarID:e.calendarID,syncToken:l?e.syncToken:void 0,pageToken:n,timeMin:e.timeMin}),i=await r(a,e.credential,e.fetchFn,e.signal);yield{items:i.items??[],nextSyncToken:i.nextSyncToken},n=i.nextPageToken,l=!1}while(null!=n)}export function createGoogleCalendarProvider(e){let n=e.fetch??globalThis.fetch,t=new Set(e.excludeCalendars??[]);async function*i(i){let r,y=s(i.boundary),f=[];for await(let l of d({credential:e.credential,fetchFn:n,signal:i.signal})){for(let e of l.items)t.has(e.id)||f.push(e);null!=l.nextSyncToken&&(r=l.nextSyncToken)}yield{modelID:o,entities:f.map(l),hasMore:!0};let m={},k=!1;for(let t=0;t<f.length;t++){let l,o=f[t],s=t===f.length-1;for await(let t of u({calendarID:o.id,credential:e.credential,fetchFn:n,timeMin:y,signal:i.signal})){null!=t.nextSyncToken&&(l=t.nextSyncToken);let e=null!=t.nextSyncToken,n=s&&e?{calendarListSyncToken:r,calendars:{...m,[o.id]:{eventsSyncToken:t.nextSyncToken}}}:void 0,i=t.items.filter(e=>"cancelled"!==e.status),d=t.items.filter(e=>"cancelled"===e.status);yield{modelID:c,entities:i.map(e=>a(e,o.id)),deleted:d.map(e=>({sourceService:"google-calendar",sourceID:e.id})),hasMore:!(s&&e),checkpoint:n},k=!0}m[o.id]={eventsSyncToken:l}}k||(yield{modelID:c,entities:[],hasMore:!1,checkpoint:{calendarListSyncToken:r,calendars:m}})}return{supportedStrategies:["on-demand"],fetchAll:i,async *fetchChanges(r){let y,f=r.checkpoint;if(null==f)return void(yield*i(r));let m=s(r.boundary),k=[],g=[];for await(let l of d({credential:e.credential,fetchFn:n,syncToken:f.calendarListSyncToken,signal:r.signal})){for(let e of l.items)!0===e.deleted?g.push(e.id):t.has(e.id)||k.push(e);null!=l.nextSyncToken&&(y=l.nextSyncToken)}yield{modelID:o,entities:k.map(l),deleted:g.map(e=>({sourceService:"google-calendar",sourceID:e})),hasMore:!0};let h={...f.calendars};for(let e of g)delete h[e];let T=k.filter(e=>!(e.id in f.calendars)),S=[...Object.keys(h).map(e=>({id:e,isNew:!1})),...T.map(e=>({id:e.id,isNew:!0}))],p={};for(let t=0;t<S.length;t++){let l,{id:i,isNew:o}=S[t],s=t===S.length-1,d=o?void 0:h[i]?.eventsSyncToken;for await(let t of u({calendarID:i,credential:e.credential,fetchFn:n,syncToken:d,timeMin:o?m:void 0,signal:r.signal})){null!=t.nextSyncToken&&(l=t.nextSyncToken);let e=null!=t.nextSyncToken,n=s&&e?{calendarListSyncToken:y,calendars:{...p,[i]:{eventsSyncToken:t.nextSyncToken}}}:void 0,o=t.items.filter(e=>"cancelled"!==e.status),r=t.items.filter(e=>"cancelled"===e.status);yield{modelID:c,entities:o.map(e=>a(e,i)),deleted:r.map(e=>({sourceService:"google-calendar",sourceID:e.id})),hasMore:!(s&&e),checkpoint:n}}p[i]={eventsSyncToken:l}}0===S.length&&(yield{modelID:c,entities:[],hasMore:!1,checkpoint:{calendarListSyncToken:y,calendars:p}})}}}
1
+ import { resolveModelID } from '@kubun/connector';
2
+ import { buildCalendarListURL, buildEventsListURL, mapCalendarToEntity, mapEventToEntity } from './api.js';
3
+ import { googleCalendarClusters } from './model.js';
4
+ const CALENDAR_MODEL_ID = resolveModelID(googleCalendarClusters, 'GoogleCalendar');
5
+ const CALENDAR_EVENT_MODEL_ID = resolveModelID(googleCalendarClusters, 'GoogleCalendarEvent');
6
+ async function fetchJSON(url, credential, fetchFn, signal) {
7
+ const response = await fetchFn(url, {
8
+ headers: {
9
+ Authorization: `Bearer ${credential.accessToken}`
10
+ },
11
+ signal
12
+ });
13
+ if (!response.ok) {
14
+ throw new Error(`Google Calendar API error: ${response.status} ${response.statusText}`);
15
+ }
16
+ return await response.json();
17
+ }
18
+ function computeTimeMin(boundary) {
19
+ if (boundary.maxAge == null) return undefined;
20
+ // Parse PnD format (e.g. "P30D" = 30 days)
21
+ const match = /^P(\d+)D$/i.exec(boundary.maxAge);
22
+ if (match == null) return undefined;
23
+ const days = Number.parseInt(match[1], 10);
24
+ const date = new Date();
25
+ date.setDate(date.getDate() - days);
26
+ return date.toISOString();
27
+ }
28
+ async function* fetchAllCalendars(params) {
29
+ let pageToken;
30
+ let isFirstPage = true;
31
+ do {
32
+ const url = buildCalendarListURL({
33
+ syncToken: isFirstPage ? params.syncToken : undefined,
34
+ pageToken
35
+ });
36
+ const data = await fetchJSON(url, params.credential, params.fetchFn, params.signal);
37
+ yield {
38
+ items: data.items ?? [],
39
+ nextSyncToken: data.nextSyncToken,
40
+ isFirstPage
41
+ };
42
+ pageToken = data.nextPageToken;
43
+ isFirstPage = false;
44
+ }while (pageToken != null)
45
+ }
46
+ async function* fetchEventsForCalendar(params) {
47
+ let pageToken;
48
+ let isFirstPage = true;
49
+ do {
50
+ const url = buildEventsListURL({
51
+ calendarID: params.calendarID,
52
+ syncToken: isFirstPage ? params.syncToken : undefined,
53
+ pageToken,
54
+ timeMin: params.timeMin
55
+ });
56
+ const data = await fetchJSON(url, params.credential, params.fetchFn, params.signal);
57
+ yield {
58
+ items: data.items ?? [],
59
+ nextSyncToken: data.nextSyncToken
60
+ };
61
+ pageToken = data.nextPageToken;
62
+ isFirstPage = false;
63
+ }while (pageToken != null)
64
+ }
65
+ export function createGoogleCalendarProvider(config) {
66
+ const fetchFn = config.fetch ?? globalThis.fetch;
67
+ const excludeSet = new Set(config.excludeCalendars ?? []);
68
+ async function* fetchAllEntities(params) {
69
+ const timeMin = computeTimeMin(params.boundary);
70
+ // Phase 1: fetch all calendars (collect all pages first, then yield single batch)
71
+ const allCalendars = [];
72
+ let calendarListSyncToken;
73
+ for await (const page of fetchAllCalendars({
74
+ credential: config.credential,
75
+ fetchFn,
76
+ signal: params.signal
77
+ })){
78
+ for (const cal of page.items){
79
+ if (!excludeSet.has(cal.id)) {
80
+ allCalendars.push(cal);
81
+ }
82
+ }
83
+ if (page.nextSyncToken != null) {
84
+ calendarListSyncToken = page.nextSyncToken;
85
+ }
86
+ }
87
+ // Yield calendar batch (no checkpoint yet — will be on the last event batch)
88
+ yield {
89
+ modelID: CALENDAR_MODEL_ID,
90
+ entities: allCalendars.map(mapCalendarToEntity),
91
+ hasMore: true
92
+ };
93
+ // Phase 2: fetch events per calendar
94
+ const calendarTokens = {};
95
+ let eventBatchYielded = false;
96
+ for(let i = 0; i < allCalendars.length; i++){
97
+ const calendar = allCalendars[i];
98
+ const isLastCalendar = i === allCalendars.length - 1;
99
+ let eventsSyncToken;
100
+ for await (const page of fetchEventsForCalendar({
101
+ calendarID: calendar.id,
102
+ credential: config.credential,
103
+ fetchFn,
104
+ timeMin,
105
+ signal: params.signal
106
+ })){
107
+ if (page.nextSyncToken != null) {
108
+ eventsSyncToken = page.nextSyncToken;
109
+ }
110
+ const isLastPage = page.nextSyncToken != null // last page has nextSyncToken
111
+ ;
112
+ const checkpoint = isLastCalendar && isLastPage ? {
113
+ calendarListSyncToken,
114
+ calendars: {
115
+ ...calendarTokens,
116
+ [calendar.id]: {
117
+ eventsSyncToken: page.nextSyncToken
118
+ }
119
+ }
120
+ } : undefined;
121
+ // Split items: cancelled → deleted, others → entities
122
+ const activeItems = page.items.filter((e)=>e.status !== 'cancelled');
123
+ const cancelledItems = page.items.filter((e)=>e.status === 'cancelled');
124
+ yield {
125
+ modelID: CALENDAR_EVENT_MODEL_ID,
126
+ entities: activeItems.map((event)=>mapEventToEntity(event, calendar.id)),
127
+ deleted: cancelledItems.map((e)=>({
128
+ sourceService: 'google-calendar',
129
+ sourceID: e.id
130
+ })),
131
+ hasMore: !(isLastCalendar && isLastPage),
132
+ checkpoint
133
+ };
134
+ eventBatchYielded = true;
135
+ }
136
+ calendarTokens[calendar.id] = {
137
+ eventsSyncToken
138
+ };
139
+ }
140
+ // If no event batches were yielded (zero calendars), emit a final empty batch
141
+ // with hasMore: false and the checkpoint so the sync can complete.
142
+ if (!eventBatchYielded) {
143
+ yield {
144
+ modelID: CALENDAR_EVENT_MODEL_ID,
145
+ entities: [],
146
+ hasMore: false,
147
+ checkpoint: {
148
+ calendarListSyncToken,
149
+ calendars: calendarTokens
150
+ }
151
+ };
152
+ }
153
+ }
154
+ return {
155
+ supportedStrategies: [
156
+ 'on-demand'
157
+ ],
158
+ fetchAll: fetchAllEntities,
159
+ async *fetchChanges (params) {
160
+ const checkpoint = params.checkpoint;
161
+ if (checkpoint == null) {
162
+ yield* fetchAllEntities(params);
163
+ return;
164
+ }
165
+ const timeMin = computeTimeMin(params.boundary);
166
+ // Phase 1: fetch calendar list changes using stored syncToken
167
+ const activeCalendars = [];
168
+ const deletedCalendarIDs = [];
169
+ let newCalendarListSyncToken;
170
+ for await (const page of fetchAllCalendars({
171
+ credential: config.credential,
172
+ fetchFn,
173
+ syncToken: checkpoint.calendarListSyncToken,
174
+ signal: params.signal
175
+ })){
176
+ for (const cal of page.items){
177
+ if (cal.deleted === true) {
178
+ deletedCalendarIDs.push(cal.id);
179
+ } else if (!excludeSet.has(cal.id)) {
180
+ activeCalendars.push(cal);
181
+ }
182
+ }
183
+ if (page.nextSyncToken != null) {
184
+ newCalendarListSyncToken = page.nextSyncToken;
185
+ }
186
+ }
187
+ // Yield calendar batch (active + deletions)
188
+ yield {
189
+ modelID: CALENDAR_MODEL_ID,
190
+ entities: activeCalendars.map(mapCalendarToEntity),
191
+ deleted: deletedCalendarIDs.map((id)=>({
192
+ sourceService: 'google-calendar',
193
+ sourceID: id
194
+ })),
195
+ hasMore: true
196
+ };
197
+ // Phase 2: determine the set of calendars to sync events for
198
+ // Start from the previous calendar set, remove deleted, add newly discovered active ones
199
+ const prevCalendarTokens = {
200
+ ...checkpoint.calendars
201
+ };
202
+ for (const deletedID of deletedCalendarIDs){
203
+ delete prevCalendarTokens[deletedID];
204
+ }
205
+ // Collect new calendars that weren't in the previous checkpoint
206
+ const newlyDiscoveredCalendars = activeCalendars.filter((cal)=>!(cal.id in checkpoint.calendars));
207
+ // All calendars to sync: known (from prev checkpoint, minus deleted) + newly discovered
208
+ const knownCalendarIDs = Object.keys(prevCalendarTokens);
209
+ // Combine: known calendar IDs (we don't have resource objects for them, only IDs)
210
+ // plus newly discovered calendar resources
211
+ const allCalendarIDsToSync = [
212
+ ...knownCalendarIDs.map((id)=>({
213
+ id,
214
+ isNew: false
215
+ })),
216
+ ...newlyDiscoveredCalendars.map((cal)=>({
217
+ id: cal.id,
218
+ isNew: true
219
+ }))
220
+ ];
221
+ const updatedCalendarTokens = {};
222
+ for(let i = 0; i < allCalendarIDsToSync.length; i++){
223
+ const { id: calendarID, isNew } = allCalendarIDsToSync[i];
224
+ const isLastCalendar = i === allCalendarIDsToSync.length - 1;
225
+ // Use stored syncToken for known calendars; use timeMin for new ones
226
+ const storedToken = isNew ? undefined : prevCalendarTokens[calendarID]?.eventsSyncToken;
227
+ let eventsSyncToken;
228
+ for await (const page of fetchEventsForCalendar({
229
+ calendarID,
230
+ credential: config.credential,
231
+ fetchFn,
232
+ syncToken: storedToken,
233
+ timeMin: isNew ? timeMin : undefined,
234
+ signal: params.signal
235
+ })){
236
+ if (page.nextSyncToken != null) {
237
+ eventsSyncToken = page.nextSyncToken;
238
+ }
239
+ const isLastPage = page.nextSyncToken != null;
240
+ const newCheckpoint = isLastCalendar && isLastPage ? {
241
+ calendarListSyncToken: newCalendarListSyncToken,
242
+ calendars: {
243
+ ...updatedCalendarTokens,
244
+ [calendarID]: {
245
+ eventsSyncToken: page.nextSyncToken
246
+ }
247
+ }
248
+ } : undefined;
249
+ // Split items: cancelled → deleted, others → entities
250
+ const activeItems = page.items.filter((e)=>e.status !== 'cancelled');
251
+ const cancelledItems = page.items.filter((e)=>e.status === 'cancelled');
252
+ yield {
253
+ modelID: CALENDAR_EVENT_MODEL_ID,
254
+ entities: activeItems.map((event)=>mapEventToEntity(event, calendarID)),
255
+ deleted: cancelledItems.map((e)=>({
256
+ sourceService: 'google-calendar',
257
+ sourceID: e.id
258
+ })),
259
+ hasMore: !(isLastCalendar && isLastPage),
260
+ checkpoint: newCheckpoint
261
+ };
262
+ }
263
+ updatedCalendarTokens[calendarID] = {
264
+ eventsSyncToken
265
+ };
266
+ }
267
+ // If no event batches were yielded (zero calendars to sync), emit a final empty
268
+ // batch with hasMore: false and the composite checkpoint so the sync can complete.
269
+ if (allCalendarIDsToSync.length === 0) {
270
+ yield {
271
+ modelID: CALENDAR_EVENT_MODEL_ID,
272
+ entities: [],
273
+ hasMore: false,
274
+ checkpoint: {
275
+ calendarListSyncToken: newCalendarListSyncToken,
276
+ calendars: updatedCalendarTokens
277
+ }
278
+ };
279
+ }
280
+ }
281
+ };
282
+ }
package/package.json CHANGED
@@ -1,34 +1,34 @@
1
1
  {
2
2
  "name": "@kubun/connector-google-calendar",
3
- "version": "0.10.0",
4
- "license": "see LICENSE.md",
3
+ "version": "0.12.0",
5
4
  "keywords": [],
5
+ "license": "see LICENSE.md",
6
+ "sideEffects": false,
6
7
  "type": "module",
7
- "main": "lib/index.js",
8
- "types": "lib/index.d.ts",
9
8
  "exports": {
10
9
  ".": "./lib/index.js"
11
10
  },
11
+ "main": "lib/index.js",
12
+ "types": "lib/index.d.ts",
12
13
  "files": [
13
14
  "lib/*",
14
15
  "LICENSE.md"
15
16
  ],
16
- "sideEffects": false,
17
17
  "dependencies": {
18
- "@kubun/connector": "^0.10.0",
19
- "@kubun/models": "^0.10.0",
20
- "@kubun/scalars": "^0.10.0",
21
- "@kubun/protocol": "^0.10.0",
22
- "@kubun/provider-google": "^0.10.0"
18
+ "@kubun/connector": "^0.12.0",
19
+ "@kubun/provider-google": "^0.12.0",
20
+ "@kubun/models": "^0.12.0",
21
+ "@kubun/protocol": "^0.12.0",
22
+ "@kubun/scalars": "^0.12.0"
23
23
  },
24
24
  "scripts": {
25
+ "build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",
25
26
  "build:clean": "del lib",
26
- "build:js": "swc src -d ./lib --config-file ../../swc.json --strip-leading-paths",
27
+ "build:js": "swc src -d ./lib --config-file ../../node_modules/@kigu/dev/swc.json --strip-leading-paths",
27
28
  "build:types": "tsc --emitDeclarationOnly --skipLibCheck",
28
29
  "build:types:ci": "tsc --emitDeclarationOnly --declarationMap false",
29
- "build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",
30
+ "test": "pnpm run test:types && pnpm run test:unit",
30
31
  "test:types": "tsc --noEmit -p tsconfig.test.json",
31
- "test:unit": "vitest run",
32
- "test": "pnpm run test:types && pnpm run test:unit"
32
+ "test:unit": "vitest run"
33
33
  }
34
34
  }