@kubun/connector-google-contacts 0.9.0 → 0.11.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 +20 -1
- package/lib/api.js +141 -1
- package/lib/connector.js +97 -1
- package/lib/index.js +5 -1
- package/lib/model.js +79 -1
- package/lib/provider.js +71 -1
- package/package.json +7 -7
package/lib/actions.js
CHANGED
|
@@ -1 +1,20 @@
|
|
|
1
|
-
import{createContact
|
|
1
|
+
import { createContact, updateContact } from './api.js';
|
|
2
|
+
export function createGoogleContactsActionHandler(config) {
|
|
3
|
+
return {
|
|
4
|
+
async create (_model, params) {
|
|
5
|
+
return createContact({
|
|
6
|
+
accessToken: config.credential.accessToken,
|
|
7
|
+
params,
|
|
8
|
+
fetch: config.fetch
|
|
9
|
+
});
|
|
10
|
+
},
|
|
11
|
+
async update (_model, sourceID, params) {
|
|
12
|
+
return updateContact({
|
|
13
|
+
accessToken: config.credential.accessToken,
|
|
14
|
+
sourceID,
|
|
15
|
+
params,
|
|
16
|
+
fetch: config.fetch
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
package/lib/api.js
CHANGED
|
@@ -1 +1,141 @@
|
|
|
1
|
-
|
|
1
|
+
const PERSON_FIELDS = 'names,emailAddresses,phoneNumbers,organizations,photos';
|
|
2
|
+
const CONTACTS_BASE_URL = 'https://people.googleapis.com/v1';
|
|
3
|
+
// A Google People API resource name is `people/{id}`. A sourceID reaches here
|
|
4
|
+
// from caller-controlled GraphQL input and is interpolated straight into the
|
|
5
|
+
// request path, so anything outside this shape (path traversal, extra segments,
|
|
6
|
+
// query injection) is rejected before a URL is built.
|
|
7
|
+
const SOURCE_ID_PATTERN = /^people\/[A-Za-z0-9_-]+$/;
|
|
8
|
+
function assertValidSourceID(sourceID) {
|
|
9
|
+
if (!SOURCE_ID_PATTERN.test(sourceID)) {
|
|
10
|
+
throw new Error(`Invalid contact sourceID: ${JSON.stringify(sourceID)}`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
async function parseGoogleError(response) {
|
|
14
|
+
try {
|
|
15
|
+
const body = await response.json();
|
|
16
|
+
return body.error?.message ?? `${response.status} ${response.statusText}`;
|
|
17
|
+
} catch {
|
|
18
|
+
return `${response.status} ${response.statusText}`;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function buildListURL(params) {
|
|
22
|
+
const url = new URL(`${CONTACTS_BASE_URL}/people/me/connections`);
|
|
23
|
+
url.searchParams.set('personFields', PERSON_FIELDS);
|
|
24
|
+
url.searchParams.set('pageSize', String(params.pageSize ?? 100));
|
|
25
|
+
if (params.syncToken != null) {
|
|
26
|
+
url.searchParams.set('syncToken', params.syncToken);
|
|
27
|
+
url.searchParams.set('requestSyncToken', 'true');
|
|
28
|
+
} else {
|
|
29
|
+
url.searchParams.set('requestSyncToken', 'true');
|
|
30
|
+
}
|
|
31
|
+
if (params.pageToken != null) {
|
|
32
|
+
url.searchParams.set('pageToken', params.pageToken);
|
|
33
|
+
}
|
|
34
|
+
return url.toString();
|
|
35
|
+
}
|
|
36
|
+
export function mapPersonToEntity(person) {
|
|
37
|
+
return {
|
|
38
|
+
sourceService: 'google-contacts',
|
|
39
|
+
sourceID: person.resourceName,
|
|
40
|
+
displayName: person.names?.[0]?.displayName ?? '',
|
|
41
|
+
email: person.emailAddresses?.[0]?.value ?? undefined,
|
|
42
|
+
phoneNumber: person.phoneNumbers?.[0]?.value ?? undefined,
|
|
43
|
+
organization: person.organizations?.[0]?.name ?? undefined,
|
|
44
|
+
jobTitle: person.organizations?.[0]?.title ?? undefined,
|
|
45
|
+
photoURL: person.photos?.[0]?.url ?? undefined,
|
|
46
|
+
sourceURL: `https://contacts.google.com/person/${person.resourceName.replace('people/', '')}`
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function buildPersonFromParams(params) {
|
|
50
|
+
const person = {};
|
|
51
|
+
if (params.displayName != null) {
|
|
52
|
+
person.names = [
|
|
53
|
+
{
|
|
54
|
+
givenName: params.displayName
|
|
55
|
+
}
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
if (params.email != null) {
|
|
59
|
+
person.emailAddresses = [
|
|
60
|
+
{
|
|
61
|
+
value: params.email
|
|
62
|
+
}
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
if (params.phoneNumber != null) {
|
|
66
|
+
person.phoneNumbers = [
|
|
67
|
+
{
|
|
68
|
+
value: params.phoneNumber
|
|
69
|
+
}
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
if (params.organization != null || params.jobTitle != null) {
|
|
73
|
+
person.organizations = [
|
|
74
|
+
{
|
|
75
|
+
...params.organization != null ? {
|
|
76
|
+
name: params.organization
|
|
77
|
+
} : {},
|
|
78
|
+
...params.jobTitle != null ? {
|
|
79
|
+
title: params.jobTitle
|
|
80
|
+
} : {}
|
|
81
|
+
}
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
return person;
|
|
85
|
+
}
|
|
86
|
+
export async function createContact(options) {
|
|
87
|
+
const fetchFn = options.fetch ?? globalThis.fetch;
|
|
88
|
+
const person = buildPersonFromParams(options.params);
|
|
89
|
+
const response = await fetchFn(`${CONTACTS_BASE_URL}/people:createContact?personFields=${PERSON_FIELDS}`, {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
headers: {
|
|
92
|
+
Authorization: `Bearer ${options.accessToken}`,
|
|
93
|
+
'Content-Type': 'application/json'
|
|
94
|
+
},
|
|
95
|
+
body: JSON.stringify(person)
|
|
96
|
+
});
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
const errorMessage = await parseGoogleError(response);
|
|
99
|
+
throw new Error(`Google People API error: ${errorMessage}`);
|
|
100
|
+
}
|
|
101
|
+
const created = await response.json();
|
|
102
|
+
return mapPersonToEntity(created);
|
|
103
|
+
}
|
|
104
|
+
export async function updateContact(options) {
|
|
105
|
+
assertValidSourceID(options.sourceID);
|
|
106
|
+
const fetchFn = options.fetch ?? globalThis.fetch;
|
|
107
|
+
// Fetch current person to get etag (required by Google People API for updates)
|
|
108
|
+
const getURL = `${CONTACTS_BASE_URL}/${options.sourceID}?personFields=${PERSON_FIELDS}`;
|
|
109
|
+
const getResponse = await fetchFn(getURL, {
|
|
110
|
+
headers: {
|
|
111
|
+
Authorization: `Bearer ${options.accessToken}`
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
if (!getResponse.ok) {
|
|
115
|
+
const errorMessage = await parseGoogleError(getResponse);
|
|
116
|
+
throw new Error(`Google People API error: ${errorMessage}`);
|
|
117
|
+
}
|
|
118
|
+
const current = await getResponse.json();
|
|
119
|
+
const person = buildPersonFromParams(options.params);
|
|
120
|
+
person.etag = current.etag;
|
|
121
|
+
const updatePersonFields = [];
|
|
122
|
+
if (person.names != null) updatePersonFields.push('names');
|
|
123
|
+
if (person.emailAddresses != null) updatePersonFields.push('emailAddresses');
|
|
124
|
+
if (person.phoneNumbers != null) updatePersonFields.push('phoneNumbers');
|
|
125
|
+
if (person.organizations != null) updatePersonFields.push('organizations');
|
|
126
|
+
const url = `${CONTACTS_BASE_URL}/${options.sourceID}:updateContact?updatePersonFields=${updatePersonFields.join(',')}&personFields=${PERSON_FIELDS}`;
|
|
127
|
+
const response = await fetchFn(url, {
|
|
128
|
+
method: 'PATCH',
|
|
129
|
+
headers: {
|
|
130
|
+
Authorization: `Bearer ${options.accessToken}`,
|
|
131
|
+
'Content-Type': 'application/json'
|
|
132
|
+
},
|
|
133
|
+
body: JSON.stringify(person)
|
|
134
|
+
});
|
|
135
|
+
if (!response.ok) {
|
|
136
|
+
const errorMessage = await parseGoogleError(response);
|
|
137
|
+
throw new Error(`Google People API error: ${errorMessage}`);
|
|
138
|
+
}
|
|
139
|
+
const updated = await response.json();
|
|
140
|
+
return mapPersonToEntity(updated);
|
|
141
|
+
}
|
package/lib/connector.js
CHANGED
|
@@ -1 +1,97 @@
|
|
|
1
|
-
import{createGoogleContactsActionHandler
|
|
1
|
+
import { createGoogleContactsActionHandler } from './actions.js';
|
|
2
|
+
import { googleContactsClusters } from './model.js';
|
|
3
|
+
import { createGoogleContactsProvider } from './provider.js';
|
|
4
|
+
export function createGoogleContactsConnector(options) {
|
|
5
|
+
return {
|
|
6
|
+
name: 'google-contacts',
|
|
7
|
+
version: '1.0.0',
|
|
8
|
+
clusters: googleContactsClusters,
|
|
9
|
+
syncStrategies: [
|
|
10
|
+
'on-demand'
|
|
11
|
+
],
|
|
12
|
+
auth: {
|
|
13
|
+
provider: 'google',
|
|
14
|
+
scopes: [
|
|
15
|
+
'https://www.googleapis.com/auth/contacts.readonly'
|
|
16
|
+
],
|
|
17
|
+
writeScopes: [
|
|
18
|
+
'https://www.googleapis.com/auth/contacts'
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
actions: [
|
|
22
|
+
{
|
|
23
|
+
name: 'create',
|
|
24
|
+
description: 'Create a new Google Contact',
|
|
25
|
+
entityModel: 'GoogleContact',
|
|
26
|
+
requiresCredential: true,
|
|
27
|
+
input: {
|
|
28
|
+
type: 'object',
|
|
29
|
+
properties: {
|
|
30
|
+
displayName: {
|
|
31
|
+
type: 'string'
|
|
32
|
+
},
|
|
33
|
+
email: {
|
|
34
|
+
type: 'string'
|
|
35
|
+
},
|
|
36
|
+
phoneNumber: {
|
|
37
|
+
type: 'string'
|
|
38
|
+
},
|
|
39
|
+
organization: {
|
|
40
|
+
type: 'string'
|
|
41
|
+
},
|
|
42
|
+
jobTitle: {
|
|
43
|
+
type: 'string'
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
required: [
|
|
47
|
+
'displayName'
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'update',
|
|
53
|
+
description: 'Update an existing Google Contact',
|
|
54
|
+
entityModel: 'GoogleContact',
|
|
55
|
+
requiresCredential: true,
|
|
56
|
+
input: {
|
|
57
|
+
type: 'object',
|
|
58
|
+
properties: {
|
|
59
|
+
sourceID: {
|
|
60
|
+
type: 'string'
|
|
61
|
+
},
|
|
62
|
+
displayName: {
|
|
63
|
+
type: 'string'
|
|
64
|
+
},
|
|
65
|
+
email: {
|
|
66
|
+
type: 'string'
|
|
67
|
+
},
|
|
68
|
+
phoneNumber: {
|
|
69
|
+
type: 'string'
|
|
70
|
+
},
|
|
71
|
+
organization: {
|
|
72
|
+
type: 'string'
|
|
73
|
+
},
|
|
74
|
+
jobTitle: {
|
|
75
|
+
type: 'string'
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
required: [
|
|
79
|
+
'sourceID'
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
],
|
|
84
|
+
actionHandler: (config)=>{
|
|
85
|
+
return createGoogleContactsActionHandler({
|
|
86
|
+
credential: config.credential,
|
|
87
|
+
fetch: options?.fetch
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
serverProvider: (config)=>{
|
|
91
|
+
return createGoogleContactsProvider({
|
|
92
|
+
credential: config.credential,
|
|
93
|
+
fetch: options?.fetch
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -1 +1,5 @@
|
|
|
1
|
-
export{createGoogleContactsActionHandler}from
|
|
1
|
+
export { createGoogleContactsActionHandler } from './actions.js';
|
|
2
|
+
export { buildListURL, buildPersonFromParams, createContact, mapPersonToEntity, updateContact } from './api.js';
|
|
3
|
+
export { createGoogleContactsConnector } from './connector.js';
|
|
4
|
+
export { googleContactDefinition, googleContactsClusters } from './model.js';
|
|
5
|
+
export { createGoogleContactsProvider } from './provider.js';
|
package/lib/model.js
CHANGED
|
@@ -1 +1,79 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { connectorClusters, connectorModelIDs } from '@kubun/connector';
|
|
2
|
+
import { modelIDs, modelsClusters } from '@kubun/models';
|
|
3
|
+
import { GraphBuilder } from '@kubun/protocol';
|
|
4
|
+
import { dateTimeSchema, urlSchema } from '@kubun/scalars';
|
|
5
|
+
export const googleContactDefinition = {
|
|
6
|
+
name: 'GoogleContact',
|
|
7
|
+
behavior: 'default',
|
|
8
|
+
interfaces: [
|
|
9
|
+
connectorModelIDs.ExternalEntity,
|
|
10
|
+
modelIDs.ContactKind
|
|
11
|
+
],
|
|
12
|
+
schema: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
displayName: {
|
|
16
|
+
type: 'string'
|
|
17
|
+
},
|
|
18
|
+
sourceService: {
|
|
19
|
+
$ref: '#/definitions/SourceService'
|
|
20
|
+
},
|
|
21
|
+
sourceID: {
|
|
22
|
+
$ref: '#/definitions/SourceID'
|
|
23
|
+
},
|
|
24
|
+
lastSyncedAt: {
|
|
25
|
+
$ref: '#/definitions/DateTime'
|
|
26
|
+
},
|
|
27
|
+
sourceURL: {
|
|
28
|
+
$ref: '#/definitions/SourceURL'
|
|
29
|
+
},
|
|
30
|
+
email: {
|
|
31
|
+
type: 'string'
|
|
32
|
+
},
|
|
33
|
+
phoneNumber: {
|
|
34
|
+
type: 'string'
|
|
35
|
+
},
|
|
36
|
+
organization: {
|
|
37
|
+
type: 'string'
|
|
38
|
+
},
|
|
39
|
+
jobTitle: {
|
|
40
|
+
type: 'string'
|
|
41
|
+
},
|
|
42
|
+
photoURL: {
|
|
43
|
+
$ref: '#/definitions/PhotoURL'
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
required: [
|
|
47
|
+
'sourceService',
|
|
48
|
+
'sourceID',
|
|
49
|
+
'lastSyncedAt'
|
|
50
|
+
],
|
|
51
|
+
additionalProperties: false,
|
|
52
|
+
definitions: {
|
|
53
|
+
SourceService: {
|
|
54
|
+
type: 'string',
|
|
55
|
+
title: 'SourceService',
|
|
56
|
+
minLength: 1,
|
|
57
|
+
maxLength: 50
|
|
58
|
+
},
|
|
59
|
+
SourceID: {
|
|
60
|
+
type: 'string',
|
|
61
|
+
title: 'SourceID',
|
|
62
|
+
minLength: 1,
|
|
63
|
+
maxLength: 500
|
|
64
|
+
},
|
|
65
|
+
SourceURL: urlSchema,
|
|
66
|
+
PhotoURL: urlSchema,
|
|
67
|
+
DateTime: dateTimeSchema
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
fieldsMeta: {}
|
|
71
|
+
};
|
|
72
|
+
const builder = new GraphBuilder({
|
|
73
|
+
externalClusters: {
|
|
74
|
+
...connectorClusters,
|
|
75
|
+
...modelsClusters
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
builder.add(googleContactDefinition);
|
|
79
|
+
export const googleContactsClusters = builder.build();
|
package/lib/provider.js
CHANGED
|
@@ -1 +1,71 @@
|
|
|
1
|
-
import{resolveModelID
|
|
1
|
+
import { resolveModelID } from '@kubun/connector';
|
|
2
|
+
import { buildListURL, mapPersonToEntity } from './api.js';
|
|
3
|
+
import { googleContactsClusters } from './model.js';
|
|
4
|
+
const GOOGLE_CONTACT_MODEL_ID = resolveModelID(googleContactsClusters, 'GoogleContact');
|
|
5
|
+
export function createGoogleContactsProvider(config) {
|
|
6
|
+
const fetchFn = config.fetch ?? globalThis.fetch;
|
|
7
|
+
async function fetchPage(params) {
|
|
8
|
+
const url = buildListURL({
|
|
9
|
+
pageToken: params.pageToken,
|
|
10
|
+
syncToken: params.syncToken
|
|
11
|
+
});
|
|
12
|
+
const response = await fetchFn(url, {
|
|
13
|
+
headers: {
|
|
14
|
+
Authorization: `Bearer ${config.credential.accessToken}`
|
|
15
|
+
},
|
|
16
|
+
signal: params.signal
|
|
17
|
+
});
|
|
18
|
+
if (!response.ok) {
|
|
19
|
+
throw new Error(`Google People API error: ${response.status} ${response.statusText}`);
|
|
20
|
+
}
|
|
21
|
+
return await response.json();
|
|
22
|
+
}
|
|
23
|
+
async function* fetchAllEntities(params) {
|
|
24
|
+
let pageToken;
|
|
25
|
+
let syncToken;
|
|
26
|
+
do {
|
|
27
|
+
const data = await fetchPage({
|
|
28
|
+
pageToken,
|
|
29
|
+
signal: params.signal
|
|
30
|
+
});
|
|
31
|
+
const entities = (data.connections ?? []).map(mapPersonToEntity);
|
|
32
|
+
pageToken = data.nextPageToken;
|
|
33
|
+
syncToken = data.nextSyncToken;
|
|
34
|
+
yield {
|
|
35
|
+
modelID: GOOGLE_CONTACT_MODEL_ID,
|
|
36
|
+
entities,
|
|
37
|
+
hasMore: pageToken != null,
|
|
38
|
+
checkpoint: syncToken
|
|
39
|
+
};
|
|
40
|
+
}while (pageToken != null)
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
supportedStrategies: [
|
|
44
|
+
'on-demand'
|
|
45
|
+
],
|
|
46
|
+
fetchAll: fetchAllEntities,
|
|
47
|
+
async *fetchChanges (params) {
|
|
48
|
+
const syncToken = params.checkpoint;
|
|
49
|
+
if (syncToken == null) {
|
|
50
|
+
yield* fetchAllEntities(params);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
let pageToken;
|
|
54
|
+
do {
|
|
55
|
+
const data = await fetchPage({
|
|
56
|
+
pageToken,
|
|
57
|
+
syncToken: pageToken != null ? undefined : syncToken,
|
|
58
|
+
signal: params.signal
|
|
59
|
+
});
|
|
60
|
+
const entities = (data.connections ?? []).map(mapPersonToEntity);
|
|
61
|
+
pageToken = data.nextPageToken;
|
|
62
|
+
yield {
|
|
63
|
+
modelID: GOOGLE_CONTACT_MODEL_ID,
|
|
64
|
+
entities,
|
|
65
|
+
hasMore: pageToken != null,
|
|
66
|
+
checkpoint: data.nextSyncToken
|
|
67
|
+
};
|
|
68
|
+
}while (pageToken != null)
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubun/connector-google-contacts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"license": "see LICENSE.md",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"type": "module",
|
|
@@ -15,15 +15,15 @@
|
|
|
15
15
|
],
|
|
16
16
|
"sideEffects": false,
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@kubun/
|
|
19
|
-
"@kubun/
|
|
20
|
-
"@kubun/
|
|
21
|
-
"@kubun/
|
|
22
|
-
"@kubun/scalars": "^0.
|
|
18
|
+
"@kubun/protocol": "^0.11.0",
|
|
19
|
+
"@kubun/connector": "^0.11.0",
|
|
20
|
+
"@kubun/models": "^0.11.0",
|
|
21
|
+
"@kubun/provider-google": "^0.11.0",
|
|
22
|
+
"@kubun/scalars": "^0.11.0"
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
25
|
"build:clean": "del lib",
|
|
26
|
-
"build:js": "swc src -d ./lib --config-file ../../swc.json --strip-leading-paths",
|
|
26
|
+
"build:js": "swc src -d ./lib --config-file ../../node_modules/@kigu/dev/swc.json --strip-leading-paths",
|
|
27
27
|
"build:types": "tsc --emitDeclarationOnly --skipLibCheck",
|
|
28
28
|
"build:types:ci": "tsc --emitDeclarationOnly --declarationMap false",
|
|
29
29
|
"build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",
|