@kubun/connector-google-contacts 0.17.3 → 0.18.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.d.ts +1 -1
- package/lib/actions.js +7 -4
- package/lib/api.d.ts +48 -14
- package/lib/api.js +150 -13
- package/lib/connector.js +25 -32
- package/lib/model.d.ts +4 -27
- package/lib/model.js +19 -34
- package/package.json +8 -8
package/lib/actions.d.ts
CHANGED
package/lib/actions.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
|
+
import { carryField, refuseCutLists } from '@kubun/connector';
|
|
1
2
|
import { createContact, updateContact } from './api.js';
|
|
2
3
|
// The People API writes a contact's organization name and job title as one
|
|
3
4
|
// `organizations` entry, so a write that changes only one of them carries the
|
|
4
5
|
// other from the target document — sending one alone would clear the other.
|
|
5
|
-
|
|
6
|
+
// A carried value trimmed at sync is refused (`carryField`), never sent.
|
|
7
|
+
function withOrganizationPair(params, existing, context) {
|
|
6
8
|
const changesOne = 'organization' in params !== 'jobTitle' in params;
|
|
7
9
|
if (!changesOne || existing == null) return params;
|
|
8
10
|
return 'organization' in params ? {
|
|
9
11
|
...params,
|
|
10
|
-
jobTitle: existing
|
|
12
|
+
jobTitle: carryField(existing, 'jobTitle', context)
|
|
11
13
|
} : {
|
|
12
14
|
...params,
|
|
13
|
-
organization: existing
|
|
15
|
+
organization: carryField(existing, 'organization', context)
|
|
14
16
|
};
|
|
15
17
|
}
|
|
16
18
|
export function createGoogleContactsActionHandler(config) {
|
|
@@ -29,10 +31,11 @@ export function createGoogleContactsActionHandler(config) {
|
|
|
29
31
|
};
|
|
30
32
|
},
|
|
31
33
|
async update (_model, sourceID, params, existing, context) {
|
|
34
|
+
refuseCutLists(params, context);
|
|
32
35
|
const record = await updateContact({
|
|
33
36
|
accessToken: config.credential.accessToken,
|
|
34
37
|
sourceID,
|
|
35
|
-
params: withOrganizationPair(params, existing),
|
|
38
|
+
params: withOrganizationPair(params, existing, context),
|
|
36
39
|
etag: context?.providerETag,
|
|
37
40
|
fetch: config.fetch
|
|
38
41
|
});
|
package/lib/api.d.ts
CHANGED
|
@@ -1,26 +1,60 @@
|
|
|
1
1
|
import { type EntityRecord } from '@kubun/connector';
|
|
2
2
|
import { type GoogleAPIClient } from '@kubun/provider-google';
|
|
3
|
+
export type GooglePersonName = {
|
|
4
|
+
displayName?: string;
|
|
5
|
+
givenName?: string;
|
|
6
|
+
familyName?: string;
|
|
7
|
+
unstructuredName?: string;
|
|
8
|
+
};
|
|
9
|
+
export type GooglePersonLabeledValue = {
|
|
10
|
+
value?: string;
|
|
11
|
+
type?: string;
|
|
12
|
+
metadata?: {
|
|
13
|
+
primary?: boolean;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
export type GooglePersonAddress = {
|
|
17
|
+
streetAddress?: string;
|
|
18
|
+
city?: string;
|
|
19
|
+
region?: string;
|
|
20
|
+
postalCode?: string;
|
|
21
|
+
country?: string;
|
|
22
|
+
type?: string;
|
|
23
|
+
metadata?: {
|
|
24
|
+
primary?: boolean;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
export type GooglePersonOrganization = {
|
|
28
|
+
name?: string;
|
|
29
|
+
title?: string;
|
|
30
|
+
};
|
|
31
|
+
export type GooglePersonBirthday = {
|
|
32
|
+
date?: {
|
|
33
|
+
year?: number;
|
|
34
|
+
month?: number;
|
|
35
|
+
day?: number;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
export type GooglePersonPhoto = {
|
|
39
|
+
url?: string;
|
|
40
|
+
default?: boolean;
|
|
41
|
+
};
|
|
3
42
|
export type GooglePersonResource = {
|
|
4
43
|
resourceName: string;
|
|
5
44
|
etag?: string;
|
|
6
|
-
names?: Array<
|
|
7
|
-
|
|
8
|
-
givenName?: string;
|
|
9
|
-
familyName?: string;
|
|
10
|
-
}>;
|
|
11
|
-
emailAddresses?: Array<{
|
|
45
|
+
names?: Array<GooglePersonName>;
|
|
46
|
+
nicknames?: Array<{
|
|
12
47
|
value?: string;
|
|
13
48
|
}>;
|
|
14
|
-
|
|
49
|
+
emailAddresses?: Array<GooglePersonLabeledValue>;
|
|
50
|
+
phoneNumbers?: Array<GooglePersonLabeledValue>;
|
|
51
|
+
addresses?: Array<GooglePersonAddress>;
|
|
52
|
+
organizations?: Array<GooglePersonOrganization>;
|
|
53
|
+
birthdays?: Array<GooglePersonBirthday>;
|
|
54
|
+
biographies?: Array<{
|
|
15
55
|
value?: string;
|
|
16
56
|
}>;
|
|
17
|
-
|
|
18
|
-
name?: string;
|
|
19
|
-
title?: string;
|
|
20
|
-
}>;
|
|
21
|
-
photos?: Array<{
|
|
22
|
-
url?: string;
|
|
23
|
-
}>;
|
|
57
|
+
photos?: Array<GooglePersonPhoto>;
|
|
24
58
|
metadata?: {
|
|
25
59
|
deleted?: boolean;
|
|
26
60
|
};
|
package/lib/api.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { classifyHTTPStatus, connectorHTTPError } from '@kubun/connector';
|
|
2
2
|
import { createGoogleAPIClient } from '@kubun/provider-google';
|
|
3
3
|
import { ContactEtagRequired, GoogleContactsError } from './errors.js';
|
|
4
|
-
const PERSON_FIELDS = 'names,emailAddresses,phoneNumbers,organizations,photos';
|
|
4
|
+
const PERSON_FIELDS = 'names,nicknames,emailAddresses,phoneNumbers,addresses,organizations,birthdays,biographies,photos';
|
|
5
5
|
const CONTACTS_BASE_URL = 'https://people.googleapis.com/v1';
|
|
6
6
|
// A Google People API resource name is `people/{id}`. A sourceID reaches here
|
|
7
7
|
// from caller-controlled GraphQL input and is interpolated straight into the
|
|
@@ -46,43 +46,164 @@ export function buildListURL(params) {
|
|
|
46
46
|
}
|
|
47
47
|
return url.toString();
|
|
48
48
|
}
|
|
49
|
+
// An empty provider string is no value: the model's text fields hold at
|
|
50
|
+
// least one character.
|
|
51
|
+
function text(value) {
|
|
52
|
+
return value == null || value === '' ? undefined : value;
|
|
53
|
+
}
|
|
54
|
+
function nonEmpty(items) {
|
|
55
|
+
return items == null || items.length === 0 ? undefined : items;
|
|
56
|
+
}
|
|
57
|
+
// Emails and phone numbers share the same labeled-value shape at Google
|
|
58
|
+
// (value, a free-text type, and a primary bit) and in the model (LabeledValue).
|
|
59
|
+
function mapLabeledValue(entry) {
|
|
60
|
+
const mapped = {
|
|
61
|
+
value: entry.value
|
|
62
|
+
};
|
|
63
|
+
const label = text(entry.type);
|
|
64
|
+
if (label != null) mapped.label = label;
|
|
65
|
+
if (entry.metadata?.primary === true) mapped.primary = true;
|
|
66
|
+
return mapped;
|
|
67
|
+
}
|
|
68
|
+
function mapAddress(entry) {
|
|
69
|
+
const mapped = {};
|
|
70
|
+
const street = text(entry.streetAddress);
|
|
71
|
+
if (street != null) mapped.street = street;
|
|
72
|
+
const locality = text(entry.city);
|
|
73
|
+
if (locality != null) mapped.locality = locality;
|
|
74
|
+
const region = text(entry.region);
|
|
75
|
+
if (region != null) mapped.region = region;
|
|
76
|
+
const postalCode = text(entry.postalCode);
|
|
77
|
+
if (postalCode != null) mapped.postalCode = postalCode;
|
|
78
|
+
const country = text(entry.country);
|
|
79
|
+
if (country != null) mapped.country = country;
|
|
80
|
+
const label = text(entry.type);
|
|
81
|
+
if (label != null) mapped.label = label;
|
|
82
|
+
if (entry.metadata?.primary === true) mapped.primary = true;
|
|
83
|
+
return mapped;
|
|
84
|
+
}
|
|
85
|
+
function pad2(value) {
|
|
86
|
+
return String(value).padStart(2, '0');
|
|
87
|
+
}
|
|
88
|
+
// Google keeps a birthday with no year as `{ month, day }` alone. The
|
|
89
|
+
// model's birthday is a single LocalDate, which cannot name a year-less day
|
|
90
|
+
// without inventing one, so a year-less birthday is left unmapped rather
|
|
91
|
+
// than guessing a year.
|
|
92
|
+
function mapBirthday(person) {
|
|
93
|
+
const date = person.birthdays?.[0]?.date;
|
|
94
|
+
if (date?.year == null || date.month == null || date.day == null) return undefined;
|
|
95
|
+
return `${date.year}-${pad2(date.month)}-${pad2(date.day)}`;
|
|
96
|
+
}
|
|
97
|
+
function mapPrimaryPhotoURL(person) {
|
|
98
|
+
const photos = person.photos ?? [];
|
|
99
|
+
return (photos.find((photo)=>photo.default === true) ?? photos[0])?.url;
|
|
100
|
+
}
|
|
49
101
|
export function mapPersonToEntity(person) {
|
|
50
102
|
return {
|
|
51
103
|
sourceService: 'google-contacts',
|
|
52
104
|
sourceID: person.resourceName,
|
|
53
105
|
displayName: person.names?.[0]?.displayName ?? '',
|
|
54
|
-
|
|
55
|
-
|
|
106
|
+
givenName: text(person.names?.[0]?.givenName),
|
|
107
|
+
familyName: text(person.names?.[0]?.familyName),
|
|
108
|
+
nickname: text(person.nicknames?.[0]?.value),
|
|
109
|
+
emails: nonEmpty(person.emailAddresses)?.map(mapLabeledValue),
|
|
110
|
+
phones: nonEmpty(person.phoneNumbers)?.map(mapLabeledValue),
|
|
111
|
+
addresses: nonEmpty(person.addresses)?.map(mapAddress),
|
|
56
112
|
organization: person.organizations?.[0]?.name ?? undefined,
|
|
57
113
|
jobTitle: person.organizations?.[0]?.title ?? undefined,
|
|
58
|
-
|
|
114
|
+
birthday: mapBirthday(person),
|
|
115
|
+
// Photos are read-only on write (the People API has no way to set a
|
|
116
|
+
// photo via updateContact), and the model keeps only the one the
|
|
117
|
+
// provider marks default (or else the first) — never the whole list.
|
|
118
|
+
photoURL: mapPrimaryPhotoURL(person),
|
|
119
|
+
note: text(person.biographies?.[0]?.value),
|
|
59
120
|
sourceURL: `https://contacts.google.com/person/${person.resourceName.replace('people/', '')}`,
|
|
60
121
|
etag: person.etag
|
|
61
122
|
};
|
|
62
123
|
}
|
|
124
|
+
function buildLabeledValues(values) {
|
|
125
|
+
return values.map((value)=>{
|
|
126
|
+
const entry = value ?? {};
|
|
127
|
+
const built = {
|
|
128
|
+
value: entry.value
|
|
129
|
+
};
|
|
130
|
+
if (typeof entry.label === 'string') built.type = entry.label;
|
|
131
|
+
if (entry.primary === true) built.metadata = {
|
|
132
|
+
primary: true
|
|
133
|
+
};
|
|
134
|
+
return built;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
function buildAddresses(values) {
|
|
138
|
+
return values.map((value)=>{
|
|
139
|
+
const entry = value ?? {};
|
|
140
|
+
const built = {};
|
|
141
|
+
if (typeof entry.street === 'string') built.streetAddress = entry.street;
|
|
142
|
+
if (typeof entry.locality === 'string') built.city = entry.locality;
|
|
143
|
+
if (typeof entry.region === 'string') built.region = entry.region;
|
|
144
|
+
if (typeof entry.postalCode === 'string') built.postalCode = entry.postalCode;
|
|
145
|
+
if (typeof entry.country === 'string') built.country = entry.country;
|
|
146
|
+
if (typeof entry.label === 'string') built.type = entry.label;
|
|
147
|
+
if (entry.primary === true) built.metadata = {
|
|
148
|
+
primary: true
|
|
149
|
+
};
|
|
150
|
+
return built;
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
const LOCAL_DATE = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
154
|
+
function buildBirthday(value) {
|
|
155
|
+
const match = LOCAL_DATE.exec(value);
|
|
156
|
+
if (match == null) {
|
|
157
|
+
throw new GoogleContactsError(`a Google Contacts birthday must be a YYYY-MM-DD date, got ${JSON.stringify(value)}`, {
|
|
158
|
+
kind: 'rejected'
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
date: {
|
|
163
|
+
year: Number(match[1] ?? ''),
|
|
164
|
+
month: Number(match[2] ?? ''),
|
|
165
|
+
day: Number(match[3] ?? '')
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
63
169
|
export function buildPersonFromParams(params) {
|
|
64
170
|
const person = {};
|
|
65
|
-
if (params.
|
|
171
|
+
if (params.givenName != null || params.familyName != null) {
|
|
66
172
|
person.names = [
|
|
67
173
|
{
|
|
68
|
-
givenName
|
|
174
|
+
...params.givenName != null ? {
|
|
175
|
+
givenName: params.givenName
|
|
176
|
+
} : {},
|
|
177
|
+
...params.familyName != null ? {
|
|
178
|
+
familyName: params.familyName
|
|
179
|
+
} : {}
|
|
69
180
|
}
|
|
70
181
|
];
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
182
|
+
} else if (params.displayName != null) {
|
|
183
|
+
// With no structured given/family name, take the caller's display name
|
|
184
|
+
// as an unstructured name rather than guessing a given/family split.
|
|
185
|
+
person.names = [
|
|
74
186
|
{
|
|
75
|
-
|
|
187
|
+
unstructuredName: params.displayName
|
|
76
188
|
}
|
|
77
189
|
];
|
|
78
190
|
}
|
|
79
|
-
if (params.
|
|
80
|
-
person.
|
|
191
|
+
if (params.nickname != null) {
|
|
192
|
+
person.nicknames = [
|
|
81
193
|
{
|
|
82
|
-
value: params.
|
|
194
|
+
value: params.nickname
|
|
83
195
|
}
|
|
84
196
|
];
|
|
85
197
|
}
|
|
198
|
+
if (Array.isArray(params.emails)) {
|
|
199
|
+
person.emailAddresses = buildLabeledValues(params.emails);
|
|
200
|
+
}
|
|
201
|
+
if (Array.isArray(params.phones)) {
|
|
202
|
+
person.phoneNumbers = buildLabeledValues(params.phones);
|
|
203
|
+
}
|
|
204
|
+
if (Array.isArray(params.addresses)) {
|
|
205
|
+
person.addresses = buildAddresses(params.addresses);
|
|
206
|
+
}
|
|
86
207
|
if (params.organization != null || params.jobTitle != null) {
|
|
87
208
|
person.organizations = [
|
|
88
209
|
{
|
|
@@ -95,6 +216,18 @@ export function buildPersonFromParams(params) {
|
|
|
95
216
|
}
|
|
96
217
|
];
|
|
97
218
|
}
|
|
219
|
+
if (params.birthday != null) {
|
|
220
|
+
person.birthdays = [
|
|
221
|
+
buildBirthday(params.birthday)
|
|
222
|
+
];
|
|
223
|
+
}
|
|
224
|
+
if (params.note != null) {
|
|
225
|
+
person.biographies = [
|
|
226
|
+
{
|
|
227
|
+
value: params.note
|
|
228
|
+
}
|
|
229
|
+
];
|
|
230
|
+
}
|
|
98
231
|
return person;
|
|
99
232
|
}
|
|
100
233
|
// Raw people.get, shared by getPerson/getPersonWithETag (fetchEntity/
|
|
@@ -143,9 +276,13 @@ export async function updateContact(options) {
|
|
|
143
276
|
person.etag = options.etag;
|
|
144
277
|
const updatePersonFields = [];
|
|
145
278
|
if (person.names != null) updatePersonFields.push('names');
|
|
279
|
+
if (person.nicknames != null) updatePersonFields.push('nicknames');
|
|
146
280
|
if (person.emailAddresses != null) updatePersonFields.push('emailAddresses');
|
|
147
281
|
if (person.phoneNumbers != null) updatePersonFields.push('phoneNumbers');
|
|
282
|
+
if (person.addresses != null) updatePersonFields.push('addresses');
|
|
148
283
|
if (person.organizations != null) updatePersonFields.push('organizations');
|
|
284
|
+
if (person.birthdays != null) updatePersonFields.push('birthdays');
|
|
285
|
+
if (person.biographies != null) updatePersonFields.push('biographies');
|
|
149
286
|
const url = `${CONTACTS_BASE_URL}/${options.sourceID}:updateContact?updatePersonFields=${updatePersonFields.join(',')}&personFields=${PERSON_FIELDS}`;
|
|
150
287
|
const client = createGoogleAPIClient({
|
|
151
288
|
api: 'Google People API',
|
package/lib/connector.js
CHANGED
|
@@ -1,7 +1,30 @@
|
|
|
1
|
+
import { contactsModelDefinitions } from '@kubun/models-contacts';
|
|
1
2
|
import { createGoogleContactsActionHandler } from './actions.js';
|
|
2
3
|
import { canonicalizeContactIntent } from './intent.js';
|
|
3
4
|
import { googleContactsClusters } from './model.js';
|
|
4
5
|
import { createGoogleContactsProvider } from './provider.js';
|
|
6
|
+
const contactKindProperties = contactsModelDefinitions.find((definition)=>definition.name === 'ContactKind')?.schema?.properties ?? {};
|
|
7
|
+
function contactKindProperty(name) {
|
|
8
|
+
const property = contactKindProperties[name];
|
|
9
|
+
if (property == null) throw new Error(`ContactKind declares no "${name}" field`);
|
|
10
|
+
return property;
|
|
11
|
+
}
|
|
12
|
+
// Every ContactKind field a write can set, in the ContactKind shape.
|
|
13
|
+
// `photoURL` is read-only (the People API has no way to set a photo via
|
|
14
|
+
// updateContact), so it is left out.
|
|
15
|
+
const contactInputProperties = {
|
|
16
|
+
displayName: contactKindProperty('displayName'),
|
|
17
|
+
givenName: contactKindProperty('givenName'),
|
|
18
|
+
familyName: contactKindProperty('familyName'),
|
|
19
|
+
nickname: contactKindProperty('nickname'),
|
|
20
|
+
emails: contactKindProperty('emails'),
|
|
21
|
+
phones: contactKindProperty('phones'),
|
|
22
|
+
addresses: contactKindProperty('addresses'),
|
|
23
|
+
organization: contactKindProperty('organization'),
|
|
24
|
+
jobTitle: contactKindProperty('jobTitle'),
|
|
25
|
+
birthday: contactKindProperty('birthday'),
|
|
26
|
+
note: contactKindProperty('note')
|
|
27
|
+
};
|
|
5
28
|
export function createGoogleContactsConnector(options) {
|
|
6
29
|
return {
|
|
7
30
|
name: 'google-contacts',
|
|
@@ -27,23 +50,7 @@ export function createGoogleContactsConnector(options) {
|
|
|
27
50
|
requiresCredential: true,
|
|
28
51
|
input: {
|
|
29
52
|
type: 'object',
|
|
30
|
-
properties:
|
|
31
|
-
displayName: {
|
|
32
|
-
type: 'string'
|
|
33
|
-
},
|
|
34
|
-
email: {
|
|
35
|
-
type: 'string'
|
|
36
|
-
},
|
|
37
|
-
phoneNumber: {
|
|
38
|
-
type: 'string'
|
|
39
|
-
},
|
|
40
|
-
organization: {
|
|
41
|
-
type: 'string'
|
|
42
|
-
},
|
|
43
|
-
jobTitle: {
|
|
44
|
-
type: 'string'
|
|
45
|
-
}
|
|
46
|
-
},
|
|
53
|
+
properties: contactInputProperties,
|
|
47
54
|
required: [
|
|
48
55
|
'displayName'
|
|
49
56
|
]
|
|
@@ -67,21 +74,7 @@ export function createGoogleContactsConnector(options) {
|
|
|
67
74
|
sourceID: {
|
|
68
75
|
type: 'string'
|
|
69
76
|
},
|
|
70
|
-
|
|
71
|
-
type: 'string'
|
|
72
|
-
},
|
|
73
|
-
email: {
|
|
74
|
-
type: 'string'
|
|
75
|
-
},
|
|
76
|
-
phoneNumber: {
|
|
77
|
-
type: 'string'
|
|
78
|
-
},
|
|
79
|
-
organization: {
|
|
80
|
-
type: 'string'
|
|
81
|
-
},
|
|
82
|
-
jobTitle: {
|
|
83
|
-
type: 'string'
|
|
84
|
-
}
|
|
77
|
+
...contactInputProperties
|
|
85
78
|
},
|
|
86
79
|
required: [
|
|
87
80
|
'sourceID'
|
package/lib/model.d.ts
CHANGED
|
@@ -1,39 +1,21 @@
|
|
|
1
1
|
export declare const googleContactDefinition: {
|
|
2
2
|
readonly name: "GoogleContact";
|
|
3
3
|
readonly behavior: "default";
|
|
4
|
-
readonly interfaces: [string, string];
|
|
4
|
+
readonly interfaces: [string, string, string];
|
|
5
5
|
readonly schema: {
|
|
6
6
|
readonly type: "object";
|
|
7
7
|
readonly properties: {
|
|
8
|
-
readonly displayName: {
|
|
9
|
-
readonly type: "string";
|
|
10
|
-
};
|
|
11
8
|
readonly sourceService: {
|
|
12
9
|
readonly $ref: "#/definitions/SourceService";
|
|
13
10
|
};
|
|
14
11
|
readonly sourceID: {
|
|
15
12
|
readonly $ref: "#/definitions/SourceID";
|
|
16
13
|
};
|
|
17
|
-
readonly lastSyncedAt: {
|
|
18
|
-
readonly $ref: "#/definitions/DateTime";
|
|
19
|
-
};
|
|
20
14
|
readonly sourceURL: {
|
|
21
15
|
readonly $ref: "#/definitions/SourceURL";
|
|
22
16
|
};
|
|
23
|
-
readonly
|
|
24
|
-
readonly
|
|
25
|
-
};
|
|
26
|
-
readonly phoneNumber: {
|
|
27
|
-
readonly type: "string";
|
|
28
|
-
};
|
|
29
|
-
readonly organization: {
|
|
30
|
-
readonly type: "string";
|
|
31
|
-
};
|
|
32
|
-
readonly jobTitle: {
|
|
33
|
-
readonly type: "string";
|
|
34
|
-
};
|
|
35
|
-
readonly photoURL: {
|
|
36
|
-
readonly $ref: "#/definitions/PhotoURL";
|
|
17
|
+
readonly lastSyncedAt: {
|
|
18
|
+
readonly $ref: "#/definitions/DateTime";
|
|
37
19
|
};
|
|
38
20
|
readonly etag: {
|
|
39
21
|
readonly type: "string";
|
|
@@ -42,11 +24,6 @@ export declare const googleContactDefinition: {
|
|
|
42
24
|
readonly required: ["sourceService", "sourceID", "lastSyncedAt"];
|
|
43
25
|
readonly additionalProperties: false;
|
|
44
26
|
readonly definitions: {
|
|
45
|
-
readonly PhotoURL: {
|
|
46
|
-
readonly type: "string";
|
|
47
|
-
readonly format: "uri";
|
|
48
|
-
readonly title: "URL";
|
|
49
|
-
};
|
|
50
27
|
readonly SourceService: {
|
|
51
28
|
readonly type: "string";
|
|
52
29
|
readonly title: "SourceService";
|
|
@@ -69,7 +46,7 @@ export declare const googleContactDefinition: {
|
|
|
69
46
|
};
|
|
70
47
|
readonly fieldsMeta: {};
|
|
71
48
|
readonly providerMergePolicy: {
|
|
72
|
-
readonly group: [
|
|
49
|
+
readonly group: [...string[], "sourceService", "sourceID", "sourceURL", "etag"];
|
|
73
50
|
};
|
|
74
51
|
};
|
|
75
52
|
export declare const googleContactsClusters: import("@kubun/protocol").ClustersRecord;
|
package/lib/model.js
CHANGED
|
@@ -1,46 +1,35 @@
|
|
|
1
1
|
import { connectorClusters, connectorModelIDs, externalEntityDefinition } from '@kubun/connector';
|
|
2
|
-
import {
|
|
2
|
+
import { contactsClusters, contactsModelDefinitions, contactsModelIDs } from '@kubun/models-contacts';
|
|
3
|
+
import { coreClusters, coreModelIDs } from '@kubun/models-core';
|
|
3
4
|
import { GraphBuilder } from '@kubun/protocol';
|
|
4
|
-
|
|
5
|
+
// The shared contact fields (displayName, givenName/familyName, nickname,
|
|
6
|
+
// emails, phones, addresses, organization, jobTitle, birthday, photoURL,
|
|
7
|
+
// note) come from ContactKind, and local tags from Taggable; only what is
|
|
8
|
+
// Google's own is declared here.
|
|
9
|
+
const contactKindProviderFields = contactsModelDefinitions.find((definition)=>definition.name === 'ContactKind')?.providerMergePolicy?.group;
|
|
10
|
+
if (contactKindProviderFields == null) throw new Error('ContactKind provider fields are missing');
|
|
5
11
|
export const googleContactDefinition = {
|
|
6
12
|
name: 'GoogleContact',
|
|
7
13
|
behavior: 'default',
|
|
8
14
|
interfaces: [
|
|
9
15
|
connectorModelIDs.ExternalEntity,
|
|
10
|
-
|
|
16
|
+
contactsModelIDs.ContactKind,
|
|
17
|
+
coreModelIDs.Taggable
|
|
11
18
|
],
|
|
12
19
|
schema: {
|
|
13
20
|
type: 'object',
|
|
14
21
|
properties: {
|
|
15
|
-
displayName: {
|
|
16
|
-
type: 'string'
|
|
17
|
-
},
|
|
18
22
|
sourceService: {
|
|
19
23
|
$ref: '#/definitions/SourceService'
|
|
20
24
|
},
|
|
21
25
|
sourceID: {
|
|
22
26
|
$ref: '#/definitions/SourceID'
|
|
23
27
|
},
|
|
24
|
-
lastSyncedAt: {
|
|
25
|
-
$ref: '#/definitions/DateTime'
|
|
26
|
-
},
|
|
27
28
|
sourceURL: {
|
|
28
29
|
$ref: '#/definitions/SourceURL'
|
|
29
30
|
},
|
|
30
|
-
|
|
31
|
-
|
|
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'
|
|
31
|
+
lastSyncedAt: {
|
|
32
|
+
$ref: '#/definitions/DateTime'
|
|
44
33
|
},
|
|
45
34
|
etag: {
|
|
46
35
|
type: 'string'
|
|
@@ -53,24 +42,19 @@ export const googleContactDefinition = {
|
|
|
53
42
|
],
|
|
54
43
|
additionalProperties: false,
|
|
55
44
|
definitions: {
|
|
56
|
-
...externalEntityDefinition.schema.definitions
|
|
57
|
-
PhotoURL: urlSchema
|
|
45
|
+
...externalEntityDefinition.schema.definitions
|
|
58
46
|
}
|
|
59
47
|
},
|
|
60
48
|
fieldsMeta: {},
|
|
61
|
-
// lastSyncedAt is a device-local sync-bookkeeping annotation
|
|
62
|
-
// provider content, so
|
|
49
|
+
// lastSyncedAt is a device-local sync-bookkeeping annotation and tags a
|
|
50
|
+
// local user annotation, neither fetched provider content, so both stay
|
|
51
|
+
// outside the group.
|
|
63
52
|
providerMergePolicy: {
|
|
64
53
|
group: [
|
|
65
|
-
|
|
54
|
+
...contactKindProviderFields,
|
|
66
55
|
'sourceService',
|
|
67
56
|
'sourceID',
|
|
68
57
|
'sourceURL',
|
|
69
|
-
'email',
|
|
70
|
-
'phoneNumber',
|
|
71
|
-
'organization',
|
|
72
|
-
'jobTitle',
|
|
73
|
-
'photoURL',
|
|
74
58
|
// The provider revision of this state: an update is written against it,
|
|
75
59
|
// so it must travel with the fields it versions.
|
|
76
60
|
'etag'
|
|
@@ -80,7 +64,8 @@ export const googleContactDefinition = {
|
|
|
80
64
|
const builder = new GraphBuilder({
|
|
81
65
|
externalClusters: {
|
|
82
66
|
...connectorClusters,
|
|
83
|
-
...
|
|
67
|
+
...coreClusters,
|
|
68
|
+
...contactsClusters
|
|
84
69
|
}
|
|
85
70
|
});
|
|
86
71
|
builder.add(googleContactDefinition);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubun/connector-google-contacts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"keywords": [],
|
|
5
5
|
"license": "see LICENSE.md",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -15,13 +15,13 @@
|
|
|
15
15
|
"LICENSE.md"
|
|
16
16
|
],
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@kubun/connector": "^0.
|
|
19
|
-
"@kubun/credential-types": "^0.
|
|
20
|
-
"@kubun/hlc": "^0.
|
|
21
|
-
"@kubun/models": "^0.
|
|
22
|
-
"@kubun/
|
|
23
|
-
"@kubun/
|
|
24
|
-
"@kubun/
|
|
18
|
+
"@kubun/connector": "^0.18.0",
|
|
19
|
+
"@kubun/credential-types": "^0.18.0",
|
|
20
|
+
"@kubun/hlc": "^0.18.0",
|
|
21
|
+
"@kubun/models-contacts": "^0.18.0",
|
|
22
|
+
"@kubun/models-core": "^0.18.0",
|
|
23
|
+
"@kubun/protocol": "^0.18.0",
|
|
24
|
+
"@kubun/provider-google": "^0.18.0"
|
|
25
25
|
},
|
|
26
26
|
"publishConfig": {
|
|
27
27
|
"access": "public"
|