// Google People API Service for Contacts
import { google } from 'googleapis';
import type { OAuth2Client } from 'google-auth-library';
import type { people_v1 } from 'googleapis';
// ─────────────────────────────────────────────────────────────────────────────
// TYPES
// ─────────────────────────────────────────────────────────────────────────────
export interface ContactName {
displayName?: string;
givenName?: string;
familyName?: string;
middleName?: string;
honorificPrefix?: string;
honorificSuffix?: string;
}
export interface ContactEmail {
value?: string;
type?: string;
formattedType?: string;
displayName?: string;
}
export interface ContactPhone {
value?: string;
type?: string;
formattedType?: string;
}
export interface ContactAddress {
formattedValue?: string;
type?: string;
streetAddress?: string;
city?: string;
region?: string;
postalCode?: string;
country?: string;
}
export interface ContactOrganization {
name?: string;
title?: string;
department?: string;
type?: string;
}
export interface ContactPhoto {
url?: string;
default?: boolean;
}
export interface Contact {
resourceName?: string;
etag?: string;
names?: ContactName[];
emailAddresses?: ContactEmail[];
phoneNumbers?: ContactPhone[];
addresses?: ContactAddress[];
organizations?: ContactOrganization[];
photos?: ContactPhoto[];
birthdays?: { date?: { year?: number; month?: number; day?: number } }[];
urls?: { value?: string; type?: string }[];
notes?: { value?: string }[];
}
export interface ContactGroup {
resourceName?: string;
etag?: string;
name?: string;
formattedName?: string;
memberCount?: number;
groupType?: string;
}
export interface CreateContactParams {
givenName: string;
familyName?: string;
middleName?: string;
email?: string;
emailType?: string;
phone?: string;
phoneType?: string;
organization?: string;
jobTitle?: string;
streetAddress?: string;
city?: string;
region?: string;
postalCode?: string;
country?: string;
notes?: string;
}
export interface UpdateContactParams {
resourceName: string;
givenName?: string;
familyName?: string;
middleName?: string;
email?: string;
emailType?: string;
phone?: string;
phoneType?: string;
organization?: string;
jobTitle?: string;
notes?: string;
}
export interface SearchContactsParams {
query: string;
pageSize?: number;
readMask?: string;
}
// Standard person fields to request
const PERSON_FIELDS = 'names,emailAddresses,phoneNumbers,addresses,organizations,photos,birthdays,urls,biographies';
// ─────────────────────────────────────────────────────────────────────────────
// SERVICE CLASS
// ─────────────────────────────────────────────────────────────────────────────
export class ContactsService {
private people: people_v1.People;
constructor(authClient: OAuth2Client) {
this.people = google.people({ version: 'v1', auth: authClient });
}
// ─────────────────────────────────────────────────────────────────────────
// CONTACTS
// ─────────────────────────────────────────────────────────────────────────
/**
* List user's contacts
*/
async listContacts(
pageSize = 100,
pageToken?: string
): Promise<{ contacts: Contact[]; nextPageToken?: string; totalItems?: number }> {
const response = await this.people.people.connections.list({
resourceName: 'people/me',
pageSize,
pageToken,
personFields: PERSON_FIELDS,
sortOrder: 'LAST_MODIFIED_DESCENDING',
});
return {
contacts: (response.data.connections || []).map(this.mapContact),
nextPageToken: response.data.nextPageToken || undefined,
totalItems: response.data.totalItems || undefined,
};
}
/**
* Get a specific contact
*/
async getContact(resourceName: string): Promise<Contact> {
const response = await this.people.people.get({
resourceName,
personFields: PERSON_FIELDS,
});
return this.mapContact(response.data);
}
/**
* Search contacts
*/
async searchContacts(params: SearchContactsParams): Promise<{ contacts: Contact[] }> {
const response = await this.people.people.searchContacts({
query: params.query,
pageSize: params.pageSize || 30,
readMask: params.readMask || PERSON_FIELDS,
});
const contacts = (response.data.results || [])
.map(r => r.person)
.filter((p): p is people_v1.Schema$Person => p !== undefined)
.map(this.mapContact);
return { contacts };
}
/**
* Create a new contact
*/
async createContact(params: CreateContactParams): Promise<Contact> {
const person: people_v1.Schema$Person = {
names: [{
givenName: params.givenName,
familyName: params.familyName,
middleName: params.middleName,
}],
};
if (params.email) {
person.emailAddresses = [{
value: params.email,
type: params.emailType || 'work',
}];
}
if (params.phone) {
person.phoneNumbers = [{
value: params.phone,
type: params.phoneType || 'mobile',
}];
}
if (params.organization || params.jobTitle) {
person.organizations = [{
name: params.organization,
title: params.jobTitle,
}];
}
if (params.streetAddress || params.city || params.country) {
person.addresses = [{
streetAddress: params.streetAddress,
city: params.city,
region: params.region,
postalCode: params.postalCode,
country: params.country,
type: 'work',
}];
}
if (params.notes) {
person.biographies = [{
value: params.notes,
contentType: 'TEXT_PLAIN',
}];
}
const response = await this.people.people.createContact({
requestBody: person,
personFields: PERSON_FIELDS,
});
return this.mapContact(response.data);
}
/**
* Update a contact
*/
async updateContact(params: UpdateContactParams): Promise<Contact> {
// First get the current contact to preserve unchanged fields
const current = await this.people.people.get({
resourceName: params.resourceName,
personFields: PERSON_FIELDS,
});
const person: people_v1.Schema$Person = {
etag: current.data.etag,
};
const updatePersonFields: string[] = [];
// Update names if provided
if (params.givenName !== undefined || params.familyName !== undefined || params.middleName !== undefined) {
person.names = [{
givenName: params.givenName ?? current.data.names?.[0]?.givenName,
familyName: params.familyName ?? current.data.names?.[0]?.familyName,
middleName: params.middleName ?? current.data.names?.[0]?.middleName,
}];
updatePersonFields.push('names');
}
// Update email if provided
if (params.email !== undefined) {
person.emailAddresses = [{
value: params.email,
type: params.emailType || current.data.emailAddresses?.[0]?.type || 'work',
}];
updatePersonFields.push('emailAddresses');
}
// Update phone if provided
if (params.phone !== undefined) {
person.phoneNumbers = [{
value: params.phone,
type: params.phoneType || current.data.phoneNumbers?.[0]?.type || 'mobile',
}];
updatePersonFields.push('phoneNumbers');
}
// Update organization if provided
if (params.organization !== undefined || params.jobTitle !== undefined) {
person.organizations = [{
name: params.organization ?? current.data.organizations?.[0]?.name,
title: params.jobTitle ?? current.data.organizations?.[0]?.title,
}];
updatePersonFields.push('organizations');
}
// Update notes if provided
if (params.notes !== undefined) {
person.biographies = [{
value: params.notes,
contentType: 'TEXT_PLAIN',
}];
updatePersonFields.push('biographies');
}
const response = await this.people.people.updateContact({
resourceName: params.resourceName,
updatePersonFields: updatePersonFields.join(','),
requestBody: person,
personFields: PERSON_FIELDS,
});
return this.mapContact(response.data);
}
/**
* Delete a contact
*/
async deleteContact(resourceName: string): Promise<void> {
await this.people.people.deleteContact({
resourceName,
});
}
// ─────────────────────────────────────────────────────────────────────────
// CONTACT GROUPS
// ─────────────────────────────────────────────────────────────────────────
/**
* List contact groups (labels)
*/
async listContactGroups(
pageSize = 100,
pageToken?: string
): Promise<{ groups: ContactGroup[]; nextPageToken?: string }> {
const response = await this.people.contactGroups.list({
pageSize,
pageToken,
});
return {
groups: (response.data.contactGroups || []).map(this.mapContactGroup),
nextPageToken: response.data.nextPageToken || undefined,
};
}
/**
* Get a contact group
*/
async getContactGroup(resourceName: string): Promise<ContactGroup> {
const response = await this.people.contactGroups.get({
resourceName,
maxMembers: 0,
});
return this.mapContactGroup(response.data);
}
/**
* Create a contact group
*/
async createContactGroup(name: string): Promise<ContactGroup> {
const response = await this.people.contactGroups.create({
requestBody: {
contactGroup: { name },
},
});
return this.mapContactGroup(response.data);
}
/**
* Update a contact group
*/
async updateContactGroup(resourceName: string, name: string): Promise<ContactGroup> {
const response = await this.people.contactGroups.update({
resourceName,
requestBody: {
contactGroup: { name },
updateGroupFields: 'name',
},
});
return this.mapContactGroup(response.data);
}
/**
* Delete a contact group
*/
async deleteContactGroup(resourceName: string, deleteContacts = false): Promise<void> {
await this.people.contactGroups.delete({
resourceName,
deleteContacts,
});
}
// ─────────────────────────────────────────────────────────────────────────
// HELPERS
// ─────────────────────────────────────────────────────────────────────────
private mapContact(data: people_v1.Schema$Person): Contact {
return {
resourceName: data.resourceName || undefined,
etag: data.etag || undefined,
names: data.names?.map(n => ({
displayName: n.displayName || undefined,
givenName: n.givenName || undefined,
familyName: n.familyName || undefined,
middleName: n.middleName || undefined,
honorificPrefix: n.honorificPrefix || undefined,
honorificSuffix: n.honorificSuffix || undefined,
})),
emailAddresses: data.emailAddresses?.map(e => ({
value: e.value || undefined,
type: e.type || undefined,
formattedType: e.formattedType || undefined,
displayName: e.displayName || undefined,
})),
phoneNumbers: data.phoneNumbers?.map(p => ({
value: p.value || undefined,
type: p.type || undefined,
formattedType: p.formattedType || undefined,
})),
addresses: data.addresses?.map(a => ({
formattedValue: a.formattedValue || undefined,
type: a.type || undefined,
streetAddress: a.streetAddress || undefined,
city: a.city || undefined,
region: a.region || undefined,
postalCode: a.postalCode || undefined,
country: a.country || undefined,
})),
organizations: data.organizations?.map(o => ({
name: o.name || undefined,
title: o.title || undefined,
department: o.department || undefined,
type: o.type || undefined,
})),
photos: data.photos?.map(p => ({
url: p.url || undefined,
default: p.default || undefined,
})),
birthdays: data.birthdays?.map(b => ({
date: b.date ? {
year: b.date.year || undefined,
month: b.date.month || undefined,
day: b.date.day || undefined,
} : undefined,
})),
urls: data.urls?.map(u => ({
value: u.value || undefined,
type: u.type || undefined,
})),
notes: data.biographies?.map(b => ({
value: b.value || undefined,
})),
};
}
private mapContactGroup(data: people_v1.Schema$ContactGroup): ContactGroup {
return {
resourceName: data.resourceName || undefined,
etag: data.etag || undefined,
name: data.name || undefined,
formattedName: data.formattedName || undefined,
memberCount: data.memberCount || undefined,
groupType: data.groupType || undefined,
};
}
}