@timesheet/plugin-google-calendar 1.2.2 → 1.3.1
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.
|
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.handleSyncBatch = void 0;
|
|
4
4
|
const integration_sdk_1 = require("@timesheet/integration-sdk");
|
|
5
5
|
const taskSync_1 = require("../lib/taskSync");
|
|
6
|
+
const SYSTEM = 'google-calendar';
|
|
7
|
+
const PROJECT_ENTITY = 'project';
|
|
6
8
|
exports.handleSyncBatch = (0, integration_sdk_1.defineHandler)(async (input, context) => {
|
|
7
9
|
context.logger.info('Processing sync batch', {
|
|
8
10
|
sinceVersion: input.sinceVersion,
|
|
@@ -10,6 +12,19 @@ exports.handleSyncBatch = (0, integration_sdk_1.defineHandler)(async (input, con
|
|
|
10
12
|
changeCount: input.changes.length,
|
|
11
13
|
hasMore: input.hasMore
|
|
12
14
|
});
|
|
15
|
+
// Pre-load all mappings once for the entire batch
|
|
16
|
+
const [projectMappings, taskMappings] = await Promise.all([
|
|
17
|
+
context.mappings.list({ system: SYSTEM, entity: PROJECT_ENTITY }),
|
|
18
|
+
context.mappings.list({ system: SYSTEM, entity: 'task' })
|
|
19
|
+
]);
|
|
20
|
+
const projectMappingByLocalId = new Map();
|
|
21
|
+
for (const mapping of projectMappings) {
|
|
22
|
+
projectMappingByLocalId.set(mapping.localId, mapping);
|
|
23
|
+
}
|
|
24
|
+
const taskMappingByLocalId = new Map();
|
|
25
|
+
for (const mapping of taskMappings) {
|
|
26
|
+
taskMappingByLocalId.set(mapping.localId, mapping);
|
|
27
|
+
}
|
|
13
28
|
let syncedCount = 0;
|
|
14
29
|
const errors = [];
|
|
15
30
|
for (const change of input.changes) {
|
|
@@ -22,7 +37,7 @@ exports.handleSyncBatch = (0, integration_sdk_1.defineHandler)(async (input, con
|
|
|
22
37
|
event,
|
|
23
38
|
taskId: change.entityId,
|
|
24
39
|
item: change.item
|
|
25
|
-
}, context);
|
|
40
|
+
}, context, { projectMappingByLocalId, taskMappingByLocalId });
|
|
26
41
|
if (result.status === 'synced' || result.status === 'deleted') {
|
|
27
42
|
syncedCount++;
|
|
28
43
|
}
|
|
@@ -37,7 +52,7 @@ exports.handleSyncBatch = (0, integration_sdk_1.defineHandler)(async (input, con
|
|
|
37
52
|
}
|
|
38
53
|
}
|
|
39
54
|
return {
|
|
40
|
-
system:
|
|
55
|
+
system: SYSTEM,
|
|
41
56
|
status: errors.length > 0 ? 'partial' : 'completed',
|
|
42
57
|
syncedCount,
|
|
43
58
|
details: {
|
|
@@ -7,6 +7,7 @@ interface GoogleCalendarClientOptions {
|
|
|
7
7
|
export declare class GoogleCalendarClient {
|
|
8
8
|
private readonly getAccessToken;
|
|
9
9
|
private readonly refreshAccessToken;
|
|
10
|
+
private cachedToken;
|
|
10
11
|
constructor(options: GoogleCalendarClientOptions);
|
|
11
12
|
testConnection(): Promise<boolean>;
|
|
12
13
|
listCalendars(): Promise<ExternalEntity[]>;
|
|
@@ -3,8 +3,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.GoogleCalendarClient = void 0;
|
|
4
4
|
class GoogleCalendarClient {
|
|
5
5
|
constructor(options) {
|
|
6
|
-
this.
|
|
7
|
-
this.
|
|
6
|
+
this.cachedToken = null;
|
|
7
|
+
this.getAccessToken = async () => {
|
|
8
|
+
if (this.cachedToken)
|
|
9
|
+
return this.cachedToken;
|
|
10
|
+
this.cachedToken = await options.getAccessToken();
|
|
11
|
+
return this.cachedToken;
|
|
12
|
+
};
|
|
13
|
+
this.refreshAccessToken = async () => {
|
|
14
|
+
this.cachedToken = null;
|
|
15
|
+
this.cachedToken = await options.refreshAccessToken();
|
|
16
|
+
return this.cachedToken;
|
|
17
|
+
};
|
|
8
18
|
}
|
|
9
19
|
async testConnection() {
|
|
10
20
|
const calendars = await this.listCalendars();
|
package/dist/lib/taskSync.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IntegrationContext } from '@timesheet/integration-sdk';
|
|
1
|
+
import { IntegrationContext, MappingRecord } from '@timesheet/integration-sdk';
|
|
2
2
|
import { GoogleCalendarConfig, GoogleCalendarSyncInput } from './types';
|
|
3
3
|
export interface GoogleCalendarSyncResult {
|
|
4
4
|
system: string;
|
|
@@ -6,7 +6,11 @@ export interface GoogleCalendarSyncResult {
|
|
|
6
6
|
syncedCount: number;
|
|
7
7
|
details?: Record<string, unknown>;
|
|
8
8
|
}
|
|
9
|
-
export
|
|
9
|
+
export interface SyncBatchCaches {
|
|
10
|
+
projectMappingByLocalId?: Map<string, MappingRecord>;
|
|
11
|
+
taskMappingByLocalId?: Map<string, MappingRecord>;
|
|
12
|
+
}
|
|
13
|
+
export declare function syncTaskToGoogleCalendar(input: GoogleCalendarSyncInput, context: IntegrationContext<GoogleCalendarConfig>, caches?: SyncBatchCaches): Promise<GoogleCalendarSyncResult>;
|
|
10
14
|
export declare function runGoogleCalendarFullSync(context: IntegrationContext<GoogleCalendarConfig>): Promise<GoogleCalendarSyncResult>;
|
|
11
15
|
export declare function handleGoogleWebhook(input: GoogleCalendarSyncInput, context: IntegrationContext<GoogleCalendarConfig>): Promise<GoogleCalendarSyncResult>;
|
|
12
16
|
export declare function ensureWatchChannels(context: IntegrationContext<GoogleCalendarConfig>): Promise<void>;
|
package/dist/lib/taskSync.js
CHANGED
|
@@ -8,7 +8,9 @@ const googleCalendarClient_1 = require("./googleCalendarClient");
|
|
|
8
8
|
const SYSTEM = 'google-calendar';
|
|
9
9
|
const PROJECT_ENTITY = 'project';
|
|
10
10
|
const TASK_ENTITY = 'task';
|
|
11
|
-
|
|
11
|
+
// Shared client instance for batch execution — avoids re-fetching the access token per change.
|
|
12
|
+
let sharedClient = null;
|
|
13
|
+
async function syncTaskToGoogleCalendar(input, context, caches) {
|
|
12
14
|
const syncDirection = context.config?.syncDirection ?? 'bidirectional';
|
|
13
15
|
if (syncDirection === 'google-to-timesheet' || syncDirection === 'external-to-timesheet') {
|
|
14
16
|
return { system: SYSTEM, status: 'skipped', syncedCount: 0, details: { reason: 'sync-direction-mismatch' } };
|
|
@@ -21,15 +23,22 @@ async function syncTaskToGoogleCalendar(input, context) {
|
|
|
21
23
|
if (!task) {
|
|
22
24
|
return { system: SYSTEM, status: 'skipped', syncedCount: 0, details: { reason: 'task-not-found' } };
|
|
23
25
|
}
|
|
24
|
-
const projectId = task.project?.id;
|
|
26
|
+
const projectId = task.project?.id ?? input.item?.projectId;
|
|
25
27
|
if (!projectId) {
|
|
26
28
|
return { system: SYSTEM, status: 'skipped', syncedCount: 0, details: { reason: 'missing-project' } };
|
|
27
29
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
30
|
+
// Use pre-loaded project mapping from cache if available
|
|
31
|
+
let projectMapping;
|
|
32
|
+
if (caches?.projectMappingByLocalId) {
|
|
33
|
+
projectMapping = caches.projectMappingByLocalId.get(projectId) ?? null;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
projectMapping = await context.mappings.get({
|
|
37
|
+
system: SYSTEM,
|
|
38
|
+
entity: PROJECT_ENTITY,
|
|
39
|
+
localId: projectId
|
|
40
|
+
});
|
|
41
|
+
}
|
|
33
42
|
const externalCalendarId = projectMapping?.externalId;
|
|
34
43
|
if (!externalCalendarId) {
|
|
35
44
|
return {
|
|
@@ -39,12 +48,19 @@ async function syncTaskToGoogleCalendar(input, context) {
|
|
|
39
48
|
details: { reason: 'missing-project-mapping', projectId }
|
|
40
49
|
};
|
|
41
50
|
}
|
|
42
|
-
const client =
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
51
|
+
const client = getOrCreateClient(context);
|
|
52
|
+
// Use pre-loaded task mapping from cache if available
|
|
53
|
+
let taskMapping;
|
|
54
|
+
if (caches?.taskMappingByLocalId) {
|
|
55
|
+
taskMapping = caches.taskMappingByLocalId.get(task.id) ?? null;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
taskMapping = await context.mappings.get({
|
|
59
|
+
system: SYSTEM,
|
|
60
|
+
entity: TASK_ENTITY,
|
|
61
|
+
localId: task.id
|
|
62
|
+
});
|
|
63
|
+
}
|
|
48
64
|
if (task.deleted) {
|
|
49
65
|
if (taskMapping?.externalId) {
|
|
50
66
|
const mappedCalendarId = getMappedCalendarId(taskMapping) ?? externalCalendarId;
|
|
@@ -88,9 +104,7 @@ async function syncTaskToGoogleCalendar(input, context) {
|
|
|
88
104
|
details: { reason: 'missing-external-id' }
|
|
89
105
|
};
|
|
90
106
|
}
|
|
91
|
-
|
|
92
|
-
system: SYSTEM,
|
|
93
|
-
entity: TASK_ENTITY,
|
|
107
|
+
const upsertedMapping = {
|
|
94
108
|
localId: task.id,
|
|
95
109
|
externalId: externalEvent.id,
|
|
96
110
|
externalLabel: externalEvent.summary ?? task.description ?? task.id,
|
|
@@ -100,7 +114,16 @@ async function syncTaskToGoogleCalendar(input, context) {
|
|
|
100
114
|
updated: externalEvent.updated ?? ''
|
|
101
115
|
},
|
|
102
116
|
syncStatus: 'SYNCED'
|
|
117
|
+
};
|
|
118
|
+
await context.mappings.upsert({
|
|
119
|
+
system: SYSTEM,
|
|
120
|
+
entity: TASK_ENTITY,
|
|
121
|
+
...upsertedMapping
|
|
103
122
|
});
|
|
123
|
+
// Update in-memory cache so subsequent changes in the same batch see this mapping
|
|
124
|
+
if (caches?.taskMappingByLocalId) {
|
|
125
|
+
caches.taskMappingByLocalId.set(task.id, upsertedMapping);
|
|
126
|
+
}
|
|
104
127
|
return {
|
|
105
128
|
system: SYSTEM,
|
|
106
129
|
status: 'synced',
|
|
@@ -392,14 +415,21 @@ function createClient(context) {
|
|
|
392
415
|
refreshAccessToken: () => context.credentials.refreshToken('google')
|
|
393
416
|
});
|
|
394
417
|
}
|
|
418
|
+
function getOrCreateClient(context) {
|
|
419
|
+
if (!sharedClient) {
|
|
420
|
+
sharedClient = createClient(context);
|
|
421
|
+
}
|
|
422
|
+
return sharedClient;
|
|
423
|
+
}
|
|
395
424
|
async function loadTask(taskId, input, context) {
|
|
425
|
+
// Prefer inline item data from sync change — avoids an API round-trip
|
|
426
|
+
if (input?.item && typeof input.item === 'object' && input.item.id) {
|
|
427
|
+
return input.item;
|
|
428
|
+
}
|
|
396
429
|
try {
|
|
397
430
|
return await context.data.getTask(taskId);
|
|
398
431
|
}
|
|
399
432
|
catch {
|
|
400
|
-
if (input?.item && typeof input.item === 'object') {
|
|
401
|
-
return input.item;
|
|
402
|
-
}
|
|
403
433
|
return null;
|
|
404
434
|
}
|
|
405
435
|
}
|
package/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "google-calendar-sync",
|
|
3
3
|
"name": "Google Calendar Sync",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.3.1",
|
|
5
5
|
"description": "Synchronize Timesheet tasks with Google Calendar",
|
|
6
6
|
"longDescription": "Bidirectional synchronization between Timesheet tasks and Google Calendar events.",
|
|
7
7
|
"icon": "google-calendar",
|