Skip to content

Commit 7dbbe48

Browse files
committed
feat: implement updateContact(contact)
1 parent 800dfbf commit 7dbbe48

File tree

4 files changed

+197
-64
lines changed

4 files changed

+197
-64
lines changed

README.md

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ console.log(`New contact was ${success ? 'saved' : 'not saved'}.`)
126126

127127
This method will return `false` if access to Contacts has not been granted.
128128

129-
### contacts.deleteContactByName(name)
129+
### contacts.deleteContact(name)
130130

131131
* `name` String (required) - The first, last, or full name of a contact.
132132

@@ -139,9 +139,38 @@ However, you should take care to specify `name` to such a degree that you can be
139139

140140
```js
141141
const name = 'Jonathan Appleseed'
142-
const deleted = contacts.deleteContactByName(name)
142+
const deleted = contacts.deleteContact(name)
143143

144144
console.log(`Contact ${name} was ${deleted ? 'deleted' : 'not deleted'}.`)
145145
```
146146

147147
This method will return `false` if access to Contacts has not been granted.
148+
149+
### contacts.updateContact(contact)
150+
151+
* `contact` Object
152+
* `firstName` String (required) - The first name of the contact.
153+
* `lastName` String (optional) - The last name of the contact.
154+
* `nickname` String (optional) - The nickname for the contact.
155+
* `birthday` String (optional) - The birthday for the contact in `YYYY-MM-DD` format.
156+
* `phoneNumbers` Array\<String\> (optional) - The phone numbers for the contact, as strings in [E.164 format](https://en.wikipedia.org/wiki/E.164): `+14155552671` or `+442071838750`.
157+
* `emailAddresses` Array\<String\> (optional) - The email addresses for the contact, as strings.
158+
159+
Returns `Boolean` - whether the contact was updated successfully.
160+
161+
Updates a contact to the user's contacts database.
162+
163+
You should take care to specify parameters to the `contact` object to such a degree that you can be confident the first contact to be returned from a predicate search is the contact you intend to update.
164+
165+
```js
166+
// Change contact's nickname from Billy -> Will
167+
const updated = contacts.updateContact({
168+
firstName: 'William',
169+
lastName: 'Grapeseed',
170+
nickname: 'Will'
171+
})
172+
173+
console.log(`Contact was ${updated ? 'updated' : 'not updated'}.`)
174+
```
175+
176+
This method will return `false` if access to Contacts has not been granted.

contacts.mm

Lines changed: 80 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,56 @@ CNAuthorizationStatus AuthStatus() {
146146
return keys;
147147
}
148148

149+
NSArray* FindContacts(const std::string& name_string) {
150+
CNContactStore *addressBook = [[CNContactStore alloc] init];
151+
152+
NSString *name = [NSString stringWithUTF8String:name_string.c_str()];
153+
NSPredicate *predicate = [CNContact predicateForContactsMatchingName:name];
154+
155+
return [addressBook unifiedContactsMatchingPredicate:predicate
156+
keysToFetch:GetContactKeys()
157+
error:nil];
158+
}
159+
160+
CNMutableContact* CreateCNMutableContact(Napi::Object contact_data) {
161+
CNMutableContact *contact = [[CNMutableContact alloc] init];
162+
163+
if (contact_data.Has("firstName")) {
164+
std::string first_name = contact_data.Get("firstName").As<Napi::String>().Utf8Value();
165+
[contact setGivenName:[NSString stringWithUTF8String:first_name.c_str()]];
166+
}
167+
168+
if (contact_data.Has("lastName")) {
169+
std::string last_name = contact_data.Get("lastName").As<Napi::String>().Utf8Value();
170+
[contact setFamilyName:[NSString stringWithUTF8String:last_name.c_str()]];
171+
}
172+
173+
if (contact_data.Has("nickname")) {
174+
std::string nick_name = contact_data.Get("nickname").As<Napi::String>().Utf8Value();
175+
[contact setNickname:[NSString stringWithUTF8String:nick_name.c_str()]];
176+
}
177+
178+
if (contact_data.Has("birthday")) {
179+
std::string birth_day = contact_data.Get("birthday").As<Napi::String>().Utf8Value();
180+
NSDateComponents *birthday_components = ParseBirthday(birth_day);
181+
[contact setBirthday:birthday_components];
182+
}
183+
184+
if (contact_data.Has("phoneNumbers")) {
185+
Napi::Array phone_number_data = contact_data.Get("phoneNumbers").As<Napi::Array>();
186+
NSArray *phone_numbers = ParsePhoneNumbers(phone_number_data);
187+
[contact setPhoneNumbers:[NSArray arrayWithArray:phone_numbers]];
188+
}
189+
190+
if (contact_data.Has("emailAddresses")) {
191+
Napi::Array email_address_data = contact_data.Get("emailAddresses").As<Napi::Array>();
192+
NSArray *email_addresses = ParseEmailAddresses(email_address_data);
193+
[contact setEmailAddresses:[NSArray arrayWithArray:email_addresses]];
194+
}
195+
196+
return contact;
197+
}
198+
149199
/***** EXPORTED FUNCTIONS *****/
150200

151201
Napi::Value GetAuthStatus(const Napi::CallbackInfo &info) {
@@ -174,8 +224,8 @@ CNAuthorizationStatus AuthStatus() {
174224

175225
NSPredicate *predicate = [CNContact predicateForContactsInContainerWithIdentifier:addressBook.defaultContainerIdentifier];
176226
NSArray *cncontacts = [addressBook unifiedContactsMatchingPredicate:predicate
177-
keysToFetch:GetContactKeys()
178-
error:nil];
227+
keysToFetch:GetContactKeys()
228+
error:nil];
179229

180230
int num_contacts = [cncontacts count];
181231
for (int i = 0; i < num_contacts; i++) {
@@ -189,18 +239,12 @@ CNAuthorizationStatus AuthStatus() {
189239
Napi::Array GetContactsByName(const Napi::CallbackInfo &info) {
190240
Napi::Env env = info.Env();
191241
Napi::Array contacts = Napi::Array::New(env);
192-
CNContactStore *addressBook = [[CNContactStore alloc] init];
193242

194243
if (AuthStatus() != CNAuthorizationStatusAuthorized)
195244
return contacts;
196245

197-
std::string name_string = info[0].As<Napi::String>().Utf8Value();
198-
NSString *name = [NSString stringWithUTF8String:name_string.c_str()];
199-
NSPredicate *predicate = [CNContact predicateForContactsMatchingName:name];
200-
201-
NSArray *cncontacts = [addressBook unifiedContactsMatchingPredicate:predicate
202-
keysToFetch:GetContactKeys()
203-
error:nil];
246+
const std::string name_string = info[0].As<Napi::String>().Utf8Value();
247+
NSArray *cncontacts = FindContacts(name_string);
204248

205249
int num_contacts = [cncontacts count];
206250
for (int i = 0; i < num_contacts; i++) {
@@ -218,71 +262,48 @@ CNAuthorizationStatus AuthStatus() {
218262
if (AuthStatus() != CNAuthorizationStatusAuthorized)
219263
return Napi::Boolean::New(env, false);
220264

221-
CNMutableContact *contact = [[CNMutableContact alloc] init];
222265
Napi::Object contact_data = info[0].As<Napi::Object>();
266+
CNMutableContact *contact = CreateCNMutableContact(contact_data);
223267

224-
if (contact_data.Has("firstName")) {
225-
std::string first_name = contact_data.Get("firstName").As<Napi::String>().Utf8Value();
226-
[contact setGivenName:[NSString stringWithUTF8String:first_name.c_str()]];
227-
}
228-
229-
if (contact_data.Has("lastName")) {
230-
std::string last_name = contact_data.Get("lastName").As<Napi::String>().Utf8Value();
231-
[contact setFamilyName:[NSString stringWithUTF8String:last_name.c_str()]];
232-
}
233-
234-
if (contact_data.Has("nickname")) {
235-
std::string nick_name = contact_data.Get("nickname").As<Napi::String>().Utf8Value();
236-
[contact setNickname:[NSString stringWithUTF8String:nick_name.c_str()]];
237-
}
268+
CNSaveRequest *request = [[CNSaveRequest alloc] init];
269+
[request addContact:contact toContainerWithIdentifier:nil];
270+
bool success = [addressBook executeSaveRequest:request error:nil];
238271

239-
if (contact_data.Has("birthday")) {
240-
std::string birth_day = contact_data.Get("birthday").As<Napi::String>().Utf8Value();
241-
NSDateComponents *birthday_components = ParseBirthday(birth_day);
242-
[contact setBirthday:birthday_components];
243-
}
272+
return Napi::Boolean::New(env, success);
273+
}
244274

245-
if (contact_data.Has("phoneNumbers")) {
246-
Napi::Array phone_number_data = contact_data.Get("phoneNumbers").As<Napi::Array>();
247-
NSArray *phone_numbers = ParsePhoneNumbers(phone_number_data);
248-
[contact setPhoneNumbers:[NSArray arrayWithArray:phone_numbers]];
249-
}
275+
Napi::Value DeleteContact(const Napi::CallbackInfo &info) {
276+
Napi::Env env = info.Env();
250277

251-
if (contact_data.Has("emailAddresses")) {
252-
Napi::Array email_address_data = contact_data.Get("emailAddresses").As<Napi::Array>();
253-
NSArray *email_addresses = ParseEmailAddresses(email_address_data);
254-
[contact setEmailAddresses:[NSArray arrayWithArray:email_addresses]];
255-
}
278+
if (AuthStatus() != CNAuthorizationStatusAuthorized)
279+
return Napi::Boolean::New(env, false);
256280

281+
const std::string name_string = info[0].As<Napi::String>().Utf8Value();
282+
NSArray *cncontacts = FindContacts(name_string);
283+
284+
CNContact *contact = (CNContact*)[cncontacts objectAtIndex:0];
257285
CNSaveRequest *request = [[CNSaveRequest alloc] init];
258-
[request addContact:contact toContainerWithIdentifier:nil];
286+
[request deleteContact:[contact mutableCopy]];
287+
288+
CNContactStore *addressBook = [[CNContactStore alloc] init];
259289
bool success = [addressBook executeSaveRequest:request error:nil];
260290

261291
return Napi::Boolean::New(env, success);
262292
}
263293

264-
Napi::Value DeleteContactByName(const Napi::CallbackInfo &info) {
294+
Napi::Value UpdateContact(const Napi::CallbackInfo &info) {
265295
Napi::Env env = info.Env();
266-
CNContactStore *addressBook = [[CNContactStore alloc] init];
267296

268297
if (AuthStatus() != CNAuthorizationStatusAuthorized)
269298
return Napi::Boolean::New(env, false);
270299

271-
std::string name_string = info[0].As<Napi::String>().Utf8Value();
272-
NSString *name = [NSString stringWithUTF8String:name_string.c_str()];
273-
NSPredicate *predicate = [CNContact predicateForContactsMatchingName:name];
274-
275-
NSArray *cncontacts = [addressBook unifiedContactsMatchingPredicate:predicate
276-
keysToFetch:GetContactKeys()
277-
error:nil];
300+
Napi::Object contact_data = info[0].As<Napi::Object>();
278301

279-
// Place the burden on end-users to specify name enough that
280-
// the first contact to be returned from a predicate search would
281-
// be the person they want to delete
282-
CNContact *contact = (CNContact*)[cncontacts objectAtIndex:0];
302+
CNMutableContact *contact = CreateCNMutableContact(contact_data);
283303
CNSaveRequest *request = [[CNSaveRequest alloc] init];
284-
[request deleteContact:[contact mutableCopy]];
304+
[request updateContact:contact];
285305

306+
CNContactStore *addressBook = [[CNContactStore alloc] init];
286307
bool success = [addressBook executeSaveRequest:request error:nil];
287308

288309
return Napi::Boolean::New(env, success);
@@ -302,7 +323,10 @@ CNAuthorizationStatus AuthStatus() {
302323
Napi::String::New(env, "addNewContact"), Napi::Function::New(env, AddNewContact)
303324
);
304325
exports.Set(
305-
Napi::String::New(env, "deleteContactByName"), Napi::Function::New(env, DeleteContactByName)
326+
Napi::String::New(env, "deleteContact"), Napi::Function::New(env, DeleteContact)
327+
);
328+
exports.Set(
329+
Napi::String::New(env, "updateContact"), Napi::Function::New(env, UpdateContact)
306330
);
307331

308332
return exports;

index.js

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,47 @@ function addNewContact(contact) {
3636
return contacts.addNewContact.call(this, contact)
3737
}
3838

39-
function deleteContactByName(name) {
39+
function updateContact(contact) {
40+
if (!contact || Object.keys(contact).length === 0) {
41+
throw new TypeError('contact must be a non-empty object')
42+
} else {
43+
const hasFirstName = contact.hasOwnProperty('firstName')
44+
const hasLastName = contact.hasOwnProperty('lastName')
45+
const hasNickname = contact.hasOwnProperty('nickname')
46+
const hasBirthday = contact.hasOwnProperty('birthday')
47+
const hasPhoneNumbers = contact.hasOwnProperty('phoneNumbers')
48+
const hasEmailAddresses = contact.hasOwnProperty('emailAddresses')
49+
50+
if (hasFirstName && typeof contact.firstName !== 'string') throw new TypeError('firstName must be a string')
51+
if (hasLastName && typeof contact.lastName !== 'string') throw new TypeError('lastName must be a string')
52+
if (hasNickname && typeof contact.nickname !== 'string') throw new TypeError('nickname must be a string')
53+
if (hasPhoneNumbers && !Array.isArray(contact.phoneNumbers)) throw new TypeError('phoneNumbers must be an array')
54+
if (hasEmailAddresses && !Array.isArray(contact.emailAddresses)) throw new TypeError('emailAddresses must be an array')
55+
56+
if (hasBirthday) {
57+
const datePattern = /^\d{4}\-(0[1-9]|1[012])\-(0[1-9]|[12][0-9]|3[01])$/
58+
if (typeof contact.birthday !== 'string') {
59+
throw new TypeError('birthday must be a string')
60+
} else if (!contact.birthday.match(datePattern)) {
61+
throw new Error('birthday must use YYYY-MM-DD format')
62+
}
63+
}
64+
}
65+
66+
return contacts.updateContact.call(this, contact)
67+
}
68+
69+
function deleteContact(name) {
4070
if (typeof name !== 'string') throw new TypeError('name must be a string')
4171

42-
return contacts.deleteContactByName.call(this, name)
72+
return contacts.deleteContact.call(this, name)
4373
}
4474

4575
module.exports = {
4676
getAuthStatus: contacts.getAuthStatus,
4777
getAllContacts: contacts.getAllContacts,
4878
getContactsByName,
4979
addNewContact,
50-
deleteContactByName
80+
deleteContact,
81+
updateContact
5182
}

test/module.spec.js

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ const {
44
getContactsByName,
55
getAllContacts,
66
addNewContact,
7-
deleteContactByName
7+
deleteContact,
8+
updateContact
89
} = require('../index')
910

1011
describe('node-mac-contacts', () => {
@@ -82,11 +83,59 @@ describe('node-mac-contacts', () => {
8283
})
8384
})
8485

85-
describe('deleteContactByName(name)', () => {
86+
describe('deleteContact(name)', () => {
8687
it('should throw if name is not a string', () => {
8788
expect(() => {
88-
deleteContactByName(12345)
89+
deleteContact(12345)
8990
}).to.throw(/name must be a string/)
9091
})
9192
})
93+
94+
describe('updateContact(contact)', () => {
95+
it('throws if contact is not a nonempty object', () => {
96+
expect(() => {
97+
updateContact(1)
98+
}).to.throw(/contact must be a non-empty object/)
99+
100+
expect(() => {
101+
updateContact({})
102+
}).to.throw(/contact must be a non-empty object/)
103+
})
104+
105+
it('should throw if name properties are not strings', () => {
106+
expect(() => {
107+
updateContact({ firstName: 1 })
108+
}).to.throw(/firstName must be a string/)
109+
110+
expect(() => {
111+
updateContact({ lastName: 1 })
112+
}).to.throw(/lastName must be a string/)
113+
114+
expect(() => {
115+
updateContact({ nickname: 1 })
116+
}).to.throw(/nickname must be a string/)
117+
})
118+
119+
it('should throw if birthday is not a string in YYYY-MM-DD format', () => {
120+
expect(() => {
121+
updateContact({ birthday: 1 })
122+
}).to.throw(/birthday must be a string/)
123+
124+
expect(() => {
125+
updateContact({ birthday: '01-01-1970' })
126+
}).to.throw(/birthday must use YYYY-MM-DD format/)
127+
})
128+
129+
it('should throw if phoneNumbers is not an array', () => {
130+
expect(() => {
131+
updateContact({ phoneNumbers: 1 })
132+
}).to.throw(/phoneNumbers must be an array/)
133+
})
134+
135+
it('should throw if emailAddresses is not an array', () => {
136+
expect(() => {
137+
updateContact({ emailAddresses: 1 })
138+
}).to.throw(/emailAddresses must be an array/)
139+
})
140+
})
92141
})

0 commit comments

Comments
 (0)