@zapier/google-contacts-connector 0.0.0 → 0.1.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/LICENSE +93 -0
- package/NOTICE +8 -0
- package/README.md +106 -2
- package/SKILL.md +135 -0
- package/cli.js +71 -0
- package/cli.ts +5 -0
- package/connections.ts +8 -0
- package/dist/cli.js +4 -0
- package/dist/index.js +1134 -0
- package/index.ts +64 -0
- package/package.json +59 -4
- package/preflight.sh +157 -0
- package/references/google-contacts-api-gotchas.md +181 -0
- package/scripts/.gitkeep +0 -0
- package/scripts/copyOtherContact.ts +57 -0
- package/scripts/createContact.ts +140 -0
- package/scripts/createContactGroup.ts +51 -0
- package/scripts/deleteContact.ts +52 -0
- package/scripts/deleteContactGroup.ts +61 -0
- package/scripts/deleteContactPhoto.ts +56 -0
- package/scripts/getContact.ts +57 -0
- package/scripts/getContactGroup.ts +71 -0
- package/scripts/listContactGroups.ts +81 -0
- package/scripts/listContacts.ts +110 -0
- package/scripts/listOtherContacts.ts +80 -0
- package/scripts/modifyContactGroupMembers.ts +82 -0
- package/scripts/searchContacts.ts +72 -0
- package/scripts/searchOtherContacts.ts +71 -0
- package/scripts/updateContact.ts +166 -0
- package/scripts/updateContactGroup.ts +67 -0
- package/scripts/updateContactPhoto.ts +64 -0
- package/tsup.config.ts +63 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { defineTool, handleIfScriptMain } from "@zapier/connectors-sdk";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
import { connectionResolvers } from "../connections.ts";
|
|
6
|
+
import {
|
|
7
|
+
PersonSchema,
|
|
8
|
+
throwForGoogleContacts,
|
|
9
|
+
} from "../lib/google-contacts.ts";
|
|
10
|
+
|
|
11
|
+
const inputSchema = z
|
|
12
|
+
.object({
|
|
13
|
+
readMask: z
|
|
14
|
+
.string()
|
|
15
|
+
.describe(
|
|
16
|
+
"Comma-separated fields to return. Other contacts support only emailAddresses, metadata, names, phoneNumbers, and photos.",
|
|
17
|
+
)
|
|
18
|
+
.default("names,emailAddresses,phoneNumbers,metadata"),
|
|
19
|
+
pageSize: z
|
|
20
|
+
.number()
|
|
21
|
+
.int()
|
|
22
|
+
.gte(1)
|
|
23
|
+
.lte(1000)
|
|
24
|
+
.describe(
|
|
25
|
+
"Max other-contacts to return per page. Defaults to 20 when omitted; pass a value when you need a specific number of results.",
|
|
26
|
+
)
|
|
27
|
+
.optional(),
|
|
28
|
+
pageToken: z
|
|
29
|
+
.string()
|
|
30
|
+
.describe(
|
|
31
|
+
"Page cursor from a previous response's next_page_token. Omit for the first page.",
|
|
32
|
+
)
|
|
33
|
+
.optional(),
|
|
34
|
+
})
|
|
35
|
+
.strict();
|
|
36
|
+
|
|
37
|
+
const definition = defineTool({
|
|
38
|
+
name: "listOtherContacts",
|
|
39
|
+
title: "List Other Contacts",
|
|
40
|
+
description:
|
|
41
|
+
'List the user\'s auto-saved "other contacts" — people interacted with (e.g. emailed) but never explicitly saved. Read-only; use copyOtherContact to make one editable.',
|
|
42
|
+
inputSchema,
|
|
43
|
+
outputSchema: z.object({
|
|
44
|
+
otherContacts: z
|
|
45
|
+
.array(PersonSchema)
|
|
46
|
+
.optional()
|
|
47
|
+
.describe("The page of other contacts."),
|
|
48
|
+
next_page_token: z
|
|
49
|
+
.string()
|
|
50
|
+
.optional()
|
|
51
|
+
.describe("Cursor for the next page; absent when there are no more."),
|
|
52
|
+
}),
|
|
53
|
+
annotations: {
|
|
54
|
+
readOnlyHint: true,
|
|
55
|
+
destructiveHint: false,
|
|
56
|
+
idempotentHint: true,
|
|
57
|
+
openWorldHint: true,
|
|
58
|
+
},
|
|
59
|
+
connection: "google-contacts",
|
|
60
|
+
run: async (input, ctx) => {
|
|
61
|
+
const url = new URL("https://people.googleapis.com/v1/otherContacts");
|
|
62
|
+
url.searchParams.set("readMask", input.readMask);
|
|
63
|
+
url.searchParams.set("pageSize", String(input.pageSize ?? 20));
|
|
64
|
+
if (input.pageToken !== undefined) {
|
|
65
|
+
url.searchParams.set("pageToken", input.pageToken);
|
|
66
|
+
}
|
|
67
|
+
const res = await ctx.fetch(url.toString(), { method: "GET" });
|
|
68
|
+
await throwForGoogleContacts(res, "listOtherContacts");
|
|
69
|
+
const payload = (await res.json()) as Record<string, unknown>;
|
|
70
|
+
if (payload && typeof payload === "object" && "nextPageToken" in payload) {
|
|
71
|
+
payload.next_page_token = payload.nextPageToken;
|
|
72
|
+
delete payload.nextPageToken;
|
|
73
|
+
}
|
|
74
|
+
return payload;
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export default definition;
|
|
79
|
+
|
|
80
|
+
await handleIfScriptMain(import.meta, definition, { connectionResolvers });
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { defineTool, handleIfScriptMain } from "@zapier/connectors-sdk";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
import { connectionResolvers } from "../connections.ts";
|
|
6
|
+
import { throwForGoogleContacts } from "../lib/google-contacts.ts";
|
|
7
|
+
|
|
8
|
+
const inputSchema = z
|
|
9
|
+
.object({
|
|
10
|
+
resourceName: z
|
|
11
|
+
.string()
|
|
12
|
+
.describe(
|
|
13
|
+
"Contact group resource name, e.g. contactGroups/1a2b3c (from listContactGroups).",
|
|
14
|
+
),
|
|
15
|
+
resourceNamesToAdd: z
|
|
16
|
+
.array(z.string())
|
|
17
|
+
.describe("Contact resource names (people/c…) to add to the group.")
|
|
18
|
+
.optional(),
|
|
19
|
+
resourceNamesToRemove: z
|
|
20
|
+
.array(z.string())
|
|
21
|
+
.describe("Contact resource names (people/c…) to remove from the group.")
|
|
22
|
+
.optional(),
|
|
23
|
+
})
|
|
24
|
+
.strict()
|
|
25
|
+
.refine(
|
|
26
|
+
(i) =>
|
|
27
|
+
(i.resourceNamesToAdd?.length ?? 0) > 0 ||
|
|
28
|
+
(i.resourceNamesToRemove?.length ?? 0) > 0,
|
|
29
|
+
{
|
|
30
|
+
message:
|
|
31
|
+
"Provide at least one of resourceNamesToAdd or resourceNamesToRemove — both cannot be empty.",
|
|
32
|
+
},
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const definition = defineTool({
|
|
36
|
+
name: "modifyContactGroupMembers",
|
|
37
|
+
title: "Modify Contact Group Members",
|
|
38
|
+
description:
|
|
39
|
+
"Add and/or remove contacts in a contact group without disturbing other memberships. Combined add+remove must be 1000 or fewer.",
|
|
40
|
+
inputSchema,
|
|
41
|
+
outputSchema: z.object({
|
|
42
|
+
notFoundResourceNames: z
|
|
43
|
+
.array(z.string())
|
|
44
|
+
.optional()
|
|
45
|
+
.describe("Resource names that could not be found and were skipped."),
|
|
46
|
+
canNotRemoveLastContactGroupResourceNames: z
|
|
47
|
+
.array(z.string())
|
|
48
|
+
.optional()
|
|
49
|
+
.describe(
|
|
50
|
+
"Contacts that could not be removed because it was their only group (every contact stays in myContacts).",
|
|
51
|
+
),
|
|
52
|
+
}),
|
|
53
|
+
annotations: {
|
|
54
|
+
readOnlyHint: false,
|
|
55
|
+
// resourceNamesToRemove removes group memberships — a non-additive update.
|
|
56
|
+
destructiveHint: true,
|
|
57
|
+
idempotentHint: false,
|
|
58
|
+
openWorldHint: true,
|
|
59
|
+
},
|
|
60
|
+
connection: "google-contacts",
|
|
61
|
+
run: async (input, ctx) => {
|
|
62
|
+
// resourceName (contactGroups/…) is a Google resource path — the slash is
|
|
63
|
+
// significant and must NOT be percent-encoded.
|
|
64
|
+
const url = `https://people.googleapis.com/v1/${input.resourceName}/members:modify`;
|
|
65
|
+
const body: Record<string, unknown> = {};
|
|
66
|
+
if (input.resourceNamesToAdd !== undefined)
|
|
67
|
+
body.resourceNamesToAdd = input.resourceNamesToAdd;
|
|
68
|
+
if (input.resourceNamesToRemove !== undefined)
|
|
69
|
+
body.resourceNamesToRemove = input.resourceNamesToRemove;
|
|
70
|
+
const res = await ctx.fetch(url, {
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers: { "Content-Type": "application/json" },
|
|
73
|
+
body: JSON.stringify(body),
|
|
74
|
+
});
|
|
75
|
+
await throwForGoogleContacts(res, "modifyContactGroupMembers");
|
|
76
|
+
return res.json();
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
export default definition;
|
|
81
|
+
|
|
82
|
+
await handleIfScriptMain(import.meta, definition, { connectionResolvers });
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { defineTool, handleIfScriptMain } from "@zapier/connectors-sdk";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
import { connectionResolvers } from "../connections.ts";
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_PERSON_FIELDS,
|
|
8
|
+
PersonSchema,
|
|
9
|
+
throwForGoogleContacts,
|
|
10
|
+
} from "../lib/google-contacts.ts";
|
|
11
|
+
|
|
12
|
+
const inputSchema = z
|
|
13
|
+
.object({
|
|
14
|
+
query: z
|
|
15
|
+
.string()
|
|
16
|
+
.describe(
|
|
17
|
+
'Prefix phrase matched against names, nicknames, emails, phones, and organizations. "foo n" matches "foo name"; "oo n" does not.',
|
|
18
|
+
),
|
|
19
|
+
readMask: z
|
|
20
|
+
.string()
|
|
21
|
+
.describe(
|
|
22
|
+
"Comma-separated list of contact fields to return on each match. Defaults to a comprehensive set.",
|
|
23
|
+
)
|
|
24
|
+
.default(DEFAULT_PERSON_FIELDS),
|
|
25
|
+
pageSize: z
|
|
26
|
+
.number()
|
|
27
|
+
.int()
|
|
28
|
+
.gte(1)
|
|
29
|
+
.lte(30)
|
|
30
|
+
.describe(
|
|
31
|
+
"Max results per page. Caps at 30. Defaults to 10 when omitted; pass a value when you need a specific number of results.",
|
|
32
|
+
)
|
|
33
|
+
.optional(),
|
|
34
|
+
})
|
|
35
|
+
.strict();
|
|
36
|
+
|
|
37
|
+
const definition = defineTool({
|
|
38
|
+
name: "searchContacts",
|
|
39
|
+
title: "Search Contacts",
|
|
40
|
+
description:
|
|
41
|
+
"Search the user's contacts by name, nickname, email, phone, or organization (prefix matching). Newly created/updated contacts may lag a few minutes behind — use listContacts when freshness matters.",
|
|
42
|
+
inputSchema,
|
|
43
|
+
outputSchema: z.object({
|
|
44
|
+
results: z
|
|
45
|
+
.array(z.object({ person: PersonSchema.optional() }))
|
|
46
|
+
.optional()
|
|
47
|
+
.describe("Matching contacts."),
|
|
48
|
+
}),
|
|
49
|
+
annotations: {
|
|
50
|
+
readOnlyHint: true,
|
|
51
|
+
destructiveHint: false,
|
|
52
|
+
idempotentHint: true,
|
|
53
|
+
openWorldHint: true,
|
|
54
|
+
},
|
|
55
|
+
connection: "google-contacts",
|
|
56
|
+
run: async (input, ctx) => {
|
|
57
|
+
const url = new URL(
|
|
58
|
+
"https://people.googleapis.com/v1/people:searchContacts",
|
|
59
|
+
);
|
|
60
|
+
url.searchParams.set("query", input.query);
|
|
61
|
+
url.searchParams.set("readMask", input.readMask);
|
|
62
|
+
url.searchParams.set("pageSize", String(input.pageSize ?? 10));
|
|
63
|
+
const res = await ctx.fetch(url.toString(), { method: "GET" });
|
|
64
|
+
await throwForGoogleContacts(res, "searchContacts");
|
|
65
|
+
// searchContacts does not paginate (no pageToken in, no cursor out).
|
|
66
|
+
return res.json();
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
export default definition;
|
|
71
|
+
|
|
72
|
+
await handleIfScriptMain(import.meta, definition, { connectionResolvers });
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { defineTool, handleIfScriptMain } from "@zapier/connectors-sdk";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
import { connectionResolvers } from "../connections.ts";
|
|
6
|
+
import {
|
|
7
|
+
PersonSchema,
|
|
8
|
+
throwForGoogleContacts,
|
|
9
|
+
} from "../lib/google-contacts.ts";
|
|
10
|
+
|
|
11
|
+
const inputSchema = z
|
|
12
|
+
.object({
|
|
13
|
+
query: z
|
|
14
|
+
.string()
|
|
15
|
+
.describe(
|
|
16
|
+
"Prefix phrase matched against names, emails, and phones of other contacts.",
|
|
17
|
+
),
|
|
18
|
+
readMask: z
|
|
19
|
+
.string()
|
|
20
|
+
.describe(
|
|
21
|
+
"Comma-separated fields to return. Other contacts support only emailAddresses, metadata, names, phoneNumbers, and photos.",
|
|
22
|
+
)
|
|
23
|
+
.default("names,emailAddresses,phoneNumbers,metadata"),
|
|
24
|
+
pageSize: z
|
|
25
|
+
.number()
|
|
26
|
+
.int()
|
|
27
|
+
.gte(1)
|
|
28
|
+
.lte(30)
|
|
29
|
+
.describe(
|
|
30
|
+
"Max results per page. Caps at 30. Defaults to 10 when omitted; pass a value when you need a specific number of results.",
|
|
31
|
+
)
|
|
32
|
+
.optional(),
|
|
33
|
+
})
|
|
34
|
+
.strict();
|
|
35
|
+
|
|
36
|
+
const definition = defineTool({
|
|
37
|
+
name: "searchOtherContacts",
|
|
38
|
+
title: "Search Other Contacts",
|
|
39
|
+
description:
|
|
40
|
+
'Search the user\'s "other contacts" by name, email, or phone (prefix matching). Same lazy-index caveat as searchContacts — recently-seen contacts may lag.',
|
|
41
|
+
inputSchema,
|
|
42
|
+
outputSchema: z.object({
|
|
43
|
+
results: z
|
|
44
|
+
.array(z.object({ person: PersonSchema.optional() }))
|
|
45
|
+
.optional()
|
|
46
|
+
.describe("Matching other contacts."),
|
|
47
|
+
}),
|
|
48
|
+
annotations: {
|
|
49
|
+
readOnlyHint: true,
|
|
50
|
+
destructiveHint: false,
|
|
51
|
+
idempotentHint: true,
|
|
52
|
+
openWorldHint: true,
|
|
53
|
+
},
|
|
54
|
+
connection: "google-contacts",
|
|
55
|
+
run: async (input, ctx) => {
|
|
56
|
+
const url = new URL(
|
|
57
|
+
"https://people.googleapis.com/v1/otherContacts:search",
|
|
58
|
+
);
|
|
59
|
+
url.searchParams.set("query", input.query);
|
|
60
|
+
url.searchParams.set("readMask", input.readMask);
|
|
61
|
+
url.searchParams.set("pageSize", String(input.pageSize ?? 10));
|
|
62
|
+
const res = await ctx.fetch(url.toString(), { method: "GET" });
|
|
63
|
+
await throwForGoogleContacts(res, "searchOtherContacts");
|
|
64
|
+
// searchOtherContacts does not paginate (no pageToken in, no cursor out).
|
|
65
|
+
return res.json();
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
export default definition;
|
|
70
|
+
|
|
71
|
+
await handleIfScriptMain(import.meta, definition, { connectionResolvers });
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// updateContact is an etag read-modify-write — the People API rejects an update
|
|
3
|
+
// without the contact's current etag (400 FAILED_PRECONDITION), so run() first GETs
|
|
4
|
+
// the contact for the fresh etag, then PATCHes :updateContact. The updatePersonFields
|
|
5
|
+
// mask is derived from exactly the fields supplied so an untouched section is never
|
|
6
|
+
// cleared (the API replaces every named field array wholesale).
|
|
7
|
+
import { defineTool, handleIfScriptMain } from "@zapier/connectors-sdk";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
|
|
10
|
+
import { connectionResolvers } from "../connections.ts";
|
|
11
|
+
import {
|
|
12
|
+
AddressInput,
|
|
13
|
+
BiographyInput,
|
|
14
|
+
BirthdayInput,
|
|
15
|
+
ContactEventInput,
|
|
16
|
+
DEFAULT_PERSON_FIELDS,
|
|
17
|
+
deriveUpdatePersonFields,
|
|
18
|
+
EmailAddressInput,
|
|
19
|
+
NameInput,
|
|
20
|
+
OrganizationInput,
|
|
21
|
+
PersonSchema,
|
|
22
|
+
PhoneNumberInput,
|
|
23
|
+
RelationInput,
|
|
24
|
+
throwForGoogleContacts,
|
|
25
|
+
UrlInput,
|
|
26
|
+
UserDefinedInput,
|
|
27
|
+
} from "../lib/google-contacts.ts";
|
|
28
|
+
|
|
29
|
+
const inputSchema = z
|
|
30
|
+
.object({
|
|
31
|
+
resourceName: z
|
|
32
|
+
.string()
|
|
33
|
+
.describe(
|
|
34
|
+
"Contact to update, e.g. people/c12345 (from listContacts or searchContacts). Pass it whole, including the people/ prefix.",
|
|
35
|
+
),
|
|
36
|
+
names: z
|
|
37
|
+
.array(NameInput)
|
|
38
|
+
.describe(
|
|
39
|
+
"Replaces the full names array. unstructuredName is rebuilt from the parts if you omit it.",
|
|
40
|
+
)
|
|
41
|
+
.optional(),
|
|
42
|
+
emailAddresses: z
|
|
43
|
+
.array(EmailAddressInput)
|
|
44
|
+
.describe(
|
|
45
|
+
"Replaces the full email list. To add one without losing the others, getContact first, append, then send the complete array.",
|
|
46
|
+
)
|
|
47
|
+
.optional(),
|
|
48
|
+
phoneNumbers: z
|
|
49
|
+
.array(PhoneNumberInput)
|
|
50
|
+
.describe(
|
|
51
|
+
"Replaces the full phone list. To add one without losing the others, getContact first, append, then send the complete array.",
|
|
52
|
+
)
|
|
53
|
+
.optional(),
|
|
54
|
+
addresses: z
|
|
55
|
+
.array(AddressInput)
|
|
56
|
+
.describe("Replaces the full addresses array.")
|
|
57
|
+
.optional(),
|
|
58
|
+
organizations: z
|
|
59
|
+
.array(OrganizationInput)
|
|
60
|
+
.describe("Replaces the full organizations array.")
|
|
61
|
+
.optional(),
|
|
62
|
+
biographies: z
|
|
63
|
+
.array(BiographyInput)
|
|
64
|
+
.describe("Replaces the full notes (biographies) array.")
|
|
65
|
+
.optional(),
|
|
66
|
+
birthdays: z
|
|
67
|
+
.array(BirthdayInput)
|
|
68
|
+
.describe("Replaces the full birthdays array.")
|
|
69
|
+
.optional(),
|
|
70
|
+
urls: z
|
|
71
|
+
.array(UrlInput)
|
|
72
|
+
.describe("Replaces the full urls array.")
|
|
73
|
+
.optional(),
|
|
74
|
+
events: z
|
|
75
|
+
.array(ContactEventInput)
|
|
76
|
+
.describe("Replaces the full custom-events array.")
|
|
77
|
+
.optional(),
|
|
78
|
+
relations: z
|
|
79
|
+
.array(RelationInput)
|
|
80
|
+
.describe("Replaces the full relations array.")
|
|
81
|
+
.optional(),
|
|
82
|
+
userDefined: z
|
|
83
|
+
.array(UserDefinedInput)
|
|
84
|
+
.describe("Replaces the full userDefined key/value array.")
|
|
85
|
+
.optional(),
|
|
86
|
+
})
|
|
87
|
+
.strict();
|
|
88
|
+
|
|
89
|
+
/** Rebuild names[].unstructuredName from the parts when the agent set name parts
|
|
90
|
+
* but not the full name, so the contact's display name doesn't go stale. */
|
|
91
|
+
function withUnstructuredNames(
|
|
92
|
+
names: z.infer<typeof NameInput>[],
|
|
93
|
+
): z.infer<typeof NameInput>[] {
|
|
94
|
+
return names.map((n) => {
|
|
95
|
+
if (n.unstructuredName) return n;
|
|
96
|
+
const parts = [
|
|
97
|
+
n.honorificPrefix,
|
|
98
|
+
n.givenName,
|
|
99
|
+
n.middleName,
|
|
100
|
+
n.familyName,
|
|
101
|
+
n.honorificSuffix,
|
|
102
|
+
].filter((p): p is string => Boolean(p && p.trim()));
|
|
103
|
+
if (parts.length === 0) return n;
|
|
104
|
+
return { ...n, unstructuredName: parts.join(" ") };
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const definition = defineTool({
|
|
109
|
+
name: "updateContact",
|
|
110
|
+
title: "Update Contact",
|
|
111
|
+
description:
|
|
112
|
+
"Update fields on an existing contact. Each array you send REPLACES that whole field (e.g. sending emailAddresses replaces every email) — to add to a list, getContact first, append, then send the full array. Fields you omit are left untouched. Does not change group membership (use modifyContactGroupMembers).",
|
|
113
|
+
inputSchema,
|
|
114
|
+
outputSchema: PersonSchema,
|
|
115
|
+
annotations: {
|
|
116
|
+
readOnlyHint: false,
|
|
117
|
+
// Each array field is replace-not-merge: sending one (e.g. emailAddresses)
|
|
118
|
+
// overwrites the whole field and silently drops any values not included.
|
|
119
|
+
// That is a non-additive (destructive) update, so the host should treat it
|
|
120
|
+
// as such and not suppress a confirmation.
|
|
121
|
+
destructiveHint: true,
|
|
122
|
+
idempotentHint: true,
|
|
123
|
+
openWorldHint: true,
|
|
124
|
+
},
|
|
125
|
+
connection: "google-contacts",
|
|
126
|
+
run: async (input, ctx) => {
|
|
127
|
+
const { resourceName, ...fields } = input;
|
|
128
|
+
const updatePersonFields = deriveUpdatePersonFields(fields);
|
|
129
|
+
if (updatePersonFields === "") {
|
|
130
|
+
throw new Error(
|
|
131
|
+
"Google Contacts updateContact: no fields to update — supply at least one of names, emailAddresses, phoneNumbers, addresses, organizations, biographies, birthdays, urls, events, relations, or userDefined.",
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// resourceName (people/c…) is a Google resource path — the slash is significant
|
|
136
|
+
// and must NOT be percent-encoded.
|
|
137
|
+
// Step 1: read the current contact for its fresh etag (required by the API).
|
|
138
|
+
const getUrl = new URL(`https://people.googleapis.com/v1/${resourceName}`);
|
|
139
|
+
getUrl.searchParams.set("personFields", "metadata");
|
|
140
|
+
const getRes = await ctx.fetch(getUrl.toString(), { method: "GET" });
|
|
141
|
+
await throwForGoogleContacts(getRes, "updateContact");
|
|
142
|
+
const current = (await getRes.json()) as { etag?: string };
|
|
143
|
+
|
|
144
|
+
// Step 2: build the PATCH body — etag + only the supplied sections; rebuild
|
|
145
|
+
// unstructuredName so the display name stays in sync with edited name parts.
|
|
146
|
+
const body: Record<string, unknown> = { etag: current.etag, ...fields };
|
|
147
|
+
if (fields.names) body.names = withUnstructuredNames(fields.names);
|
|
148
|
+
|
|
149
|
+
const patchUrl = new URL(
|
|
150
|
+
`https://people.googleapis.com/v1/${resourceName}:updateContact`,
|
|
151
|
+
);
|
|
152
|
+
patchUrl.searchParams.set("updatePersonFields", updatePersonFields);
|
|
153
|
+
patchUrl.searchParams.set("personFields", DEFAULT_PERSON_FIELDS);
|
|
154
|
+
const patchRes = await ctx.fetch(patchUrl.toString(), {
|
|
155
|
+
method: "PATCH",
|
|
156
|
+
headers: { "Content-Type": "application/json" },
|
|
157
|
+
body: JSON.stringify(body),
|
|
158
|
+
});
|
|
159
|
+
await throwForGoogleContacts(patchRes, "updateContact");
|
|
160
|
+
return patchRes.json();
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
export default definition;
|
|
165
|
+
|
|
166
|
+
await handleIfScriptMain(import.meta, definition, { connectionResolvers });
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// updateContactGroup is an etag read-modify-write — contactGroups.update (PUT) requires
|
|
3
|
+
// the group's current etag, so run() first GETs the group for the fresh etag, then PUTs.
|
|
4
|
+
import { defineTool, handleIfScriptMain } from "@zapier/connectors-sdk";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
import { connectionResolvers } from "../connections.ts";
|
|
8
|
+
import {
|
|
9
|
+
ContactGroupSchema,
|
|
10
|
+
throwForGoogleContacts,
|
|
11
|
+
} from "../lib/google-contacts.ts";
|
|
12
|
+
|
|
13
|
+
const inputSchema = z
|
|
14
|
+
.object({
|
|
15
|
+
resourceName: z
|
|
16
|
+
.string()
|
|
17
|
+
.describe(
|
|
18
|
+
"Contact group to rename, e.g. contactGroups/1a2b3c (from listContactGroups). System groups (myContacts, starred) cannot be renamed.",
|
|
19
|
+
),
|
|
20
|
+
name: z.string().describe("The new contact group (label) name."),
|
|
21
|
+
})
|
|
22
|
+
.strict();
|
|
23
|
+
|
|
24
|
+
const definition = defineTool({
|
|
25
|
+
name: "updateContactGroup",
|
|
26
|
+
title: "Update Contact Group",
|
|
27
|
+
description:
|
|
28
|
+
"Rename an existing user contact group (label). Only USER_CONTACT_GROUPs can be renamed — system groups (myContacts, starred) are rejected. Check groupType via listContactGroups.",
|
|
29
|
+
inputSchema,
|
|
30
|
+
outputSchema: ContactGroupSchema,
|
|
31
|
+
annotations: {
|
|
32
|
+
readOnlyHint: false,
|
|
33
|
+
// Rename overwrites the existing group name (replace, not add) — non-additive.
|
|
34
|
+
destructiveHint: true,
|
|
35
|
+
idempotentHint: true,
|
|
36
|
+
openWorldHint: true,
|
|
37
|
+
},
|
|
38
|
+
connection: "google-contacts",
|
|
39
|
+
run: async (input, ctx) => {
|
|
40
|
+
// resourceName (contactGroups/…) is a Google resource path — the slash is
|
|
41
|
+
// significant and must NOT be percent-encoded.
|
|
42
|
+
const base = `https://people.googleapis.com/v1/${input.resourceName}`;
|
|
43
|
+
|
|
44
|
+
// Step 1: read the current group for its fresh etag (required by the API).
|
|
45
|
+
const getUrl = new URL(base);
|
|
46
|
+
getUrl.searchParams.set("groupFields", "metadata");
|
|
47
|
+
const getRes = await ctx.fetch(getUrl.toString(), { method: "GET" });
|
|
48
|
+
await throwForGoogleContacts(getRes, "updateContactGroup");
|
|
49
|
+
const current = (await getRes.json()) as { etag?: string };
|
|
50
|
+
|
|
51
|
+
// Step 2: PUT the new name with the etag.
|
|
52
|
+
const putRes = await ctx.fetch(base, {
|
|
53
|
+
method: "PUT",
|
|
54
|
+
headers: { "Content-Type": "application/json" },
|
|
55
|
+
body: JSON.stringify({
|
|
56
|
+
contactGroup: { etag: current.etag, name: input.name },
|
|
57
|
+
updateGroupFields: "name",
|
|
58
|
+
}),
|
|
59
|
+
});
|
|
60
|
+
await throwForGoogleContacts(putRes, "updateContactGroup");
|
|
61
|
+
return putRes.json();
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export default definition;
|
|
66
|
+
|
|
67
|
+
await handleIfScriptMain(import.meta, definition, { connectionResolvers });
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { defineTool, handleIfScriptMain } from "@zapier/connectors-sdk";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
import { connectionResolvers } from "../connections.ts";
|
|
6
|
+
import {
|
|
7
|
+
PersonResponseSchema,
|
|
8
|
+
throwForGoogleContacts,
|
|
9
|
+
} from "../lib/google-contacts.ts";
|
|
10
|
+
|
|
11
|
+
const inputSchema = z
|
|
12
|
+
.object({
|
|
13
|
+
resourceName: z
|
|
14
|
+
.string()
|
|
15
|
+
.describe(
|
|
16
|
+
"Contact resource name, e.g. people/c12345 (from listContacts or searchContacts). Pass it whole, including the people/ prefix.",
|
|
17
|
+
),
|
|
18
|
+
photoBytes: z
|
|
19
|
+
.string()
|
|
20
|
+
.describe("The contact photo as a base64-encoded image string."),
|
|
21
|
+
personFields: z
|
|
22
|
+
.string()
|
|
23
|
+
.describe(
|
|
24
|
+
"Which fields to return on the updated contact. Defaults to a comprehensive set.",
|
|
25
|
+
)
|
|
26
|
+
.default("names,emailAddresses,phoneNumbers,photos,metadata"),
|
|
27
|
+
})
|
|
28
|
+
.strict();
|
|
29
|
+
|
|
30
|
+
const definition = defineTool({
|
|
31
|
+
name: "updateContactPhoto",
|
|
32
|
+
title: "Update Contact Photo",
|
|
33
|
+
description:
|
|
34
|
+
"Set or replace a contact's photo. photoBytes is the image as a base64-encoded string (not a URL or file path).",
|
|
35
|
+
inputSchema,
|
|
36
|
+
outputSchema: PersonResponseSchema,
|
|
37
|
+
annotations: {
|
|
38
|
+
readOnlyHint: false,
|
|
39
|
+
// Overwrites the contact's existing photo (replace, not add) — non-additive.
|
|
40
|
+
destructiveHint: true,
|
|
41
|
+
idempotentHint: true,
|
|
42
|
+
openWorldHint: true,
|
|
43
|
+
},
|
|
44
|
+
connection: "google-contacts",
|
|
45
|
+
run: async (input, ctx) => {
|
|
46
|
+
// resourceName (people/c…) is a Google resource path — the slash is significant
|
|
47
|
+
// and must NOT be percent-encoded.
|
|
48
|
+
const url = `https://people.googleapis.com/v1/${input.resourceName}:updateContactPhoto`;
|
|
49
|
+
const res = await ctx.fetch(url, {
|
|
50
|
+
method: "PATCH",
|
|
51
|
+
headers: { "Content-Type": "application/json" },
|
|
52
|
+
body: JSON.stringify({
|
|
53
|
+
photoBytes: input.photoBytes,
|
|
54
|
+
personFields: input.personFields,
|
|
55
|
+
}),
|
|
56
|
+
});
|
|
57
|
+
await throwForGoogleContacts(res, "updateContactPhoto");
|
|
58
|
+
return res.json();
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export default definition;
|
|
63
|
+
|
|
64
|
+
await handleIfScriptMain(import.meta, definition, { connectionResolvers });
|
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tsup build config for connectors.
|
|
3
|
+
*
|
|
4
|
+
* Compiles index.ts (library entry, bundled) and cli.ts (CLI, bundled but
|
|
5
|
+
* with the connector's index module kept external).
|
|
6
|
+
*
|
|
7
|
+
* The cli.ts entry externalises ./index.ts → ./index.js via an esbuild plugin
|
|
8
|
+
* so that dist/cli.js imports the pre-built dist/index.js as a separate module
|
|
9
|
+
* rather than inlining scripts. Without this, each script's top-level
|
|
10
|
+
* `await handleIfScriptMain(import.meta, …)` ends up inside the single-file
|
|
11
|
+
* bundle; in Node 22.18+ import.meta.main is true for the bundle entry,
|
|
12
|
+
* causing every script to execute when the dispatch CLI starts instead of
|
|
13
|
+
* routing via runDispatchCli.
|
|
14
|
+
*
|
|
15
|
+
* Managed by @zapier/connectors-dev — do not edit; synced byte-for-byte
|
|
16
|
+
* across every connector.
|
|
17
|
+
*/
|
|
18
|
+
import { defineConfig } from "tsup";
|
|
19
|
+
|
|
20
|
+
export default defineConfig([
|
|
21
|
+
{
|
|
22
|
+
entry: ["index.ts"],
|
|
23
|
+
format: ["esm"],
|
|
24
|
+
dts: false,
|
|
25
|
+
clean: true,
|
|
26
|
+
target: "es2022",
|
|
27
|
+
external: [
|
|
28
|
+
"@modelcontextprotocol/sdk",
|
|
29
|
+
"@zapier/zapier-sdk",
|
|
30
|
+
"zod",
|
|
31
|
+
"@zapier/connectors-sdk",
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
entry: ["cli.ts"],
|
|
36
|
+
format: ["esm"],
|
|
37
|
+
dts: false,
|
|
38
|
+
clean: false,
|
|
39
|
+
target: "es2022",
|
|
40
|
+
external: [
|
|
41
|
+
"@modelcontextprotocol/sdk",
|
|
42
|
+
"@zapier/zapier-sdk",
|
|
43
|
+
"zod",
|
|
44
|
+
"@zapier/connectors-sdk",
|
|
45
|
+
],
|
|
46
|
+
esbuildPlugins: [
|
|
47
|
+
{
|
|
48
|
+
name: "externalize-connector-index",
|
|
49
|
+
setup(build) {
|
|
50
|
+
// Resolve ./index.ts to the pre-built ./index.js and mark it external
|
|
51
|
+
// so scripts are not inlined into dist/cli.js. When dist/index.js is
|
|
52
|
+
// imported (not the entry), import.meta.main is false inside it and
|
|
53
|
+
// handleIfScriptMain's existing !meta.main guard suppresses per-script
|
|
54
|
+
// execution — no SDK changes required.
|
|
55
|
+
build.onResolve({ filter: /^\.\/index(\.ts)?$/ }, () => ({
|
|
56
|
+
path: "./index.js",
|
|
57
|
+
external: true,
|
|
58
|
+
}));
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
]);
|