2014-08-12 10:24:07 +00:00
|
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
|
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
"use strict";
|
|
|
|
|
|
|
|
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
|
|
|
|
|
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "console",
|
|
|
|
"resource://gre/modules/devtools/Console.jsm");
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "LoopStorage",
|
|
|
|
"resource:///modules/loop/LoopStorage.jsm");
|
2014-08-29 18:31:46 +00:00
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "CardDavImporter",
|
|
|
|
"resource:///modules/loop/CardDavImporter.jsm");
|
2014-08-12 10:24:07 +00:00
|
|
|
XPCOMUtils.defineLazyGetter(this, "eventEmitter", function() {
|
|
|
|
const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js", {});
|
|
|
|
return new EventEmitter();
|
|
|
|
});
|
|
|
|
|
|
|
|
this.EXPORTED_SYMBOLS = ["LoopContacts"];
|
|
|
|
|
|
|
|
const kObjectStoreName = "contacts";
|
|
|
|
|
|
|
|
/*
|
|
|
|
* The table used to store contacts information contains two identifiers,
|
|
|
|
* both of which can be used to look up entries in the table. The table
|
|
|
|
* key path (primary index, which must be unique) is "_guid", and is
|
|
|
|
* automatically generated by IndexedDB when an entry is first inserted.
|
|
|
|
* The other identifier, "id", is the supposedly unique key assigned to this
|
|
|
|
* entry by whatever service generated it (e.g., Google Contacts). While
|
|
|
|
* this key should, in theory, be completely unique, we don't use it
|
|
|
|
* as the key path to avoid generating errors when an external database
|
|
|
|
* violates this constraint. This second ID is referred to as the "serviceId".
|
|
|
|
*/
|
|
|
|
const kKeyPath = "_guid";
|
|
|
|
const kServiceIdIndex = "id";
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Contacts validation.
|
|
|
|
*
|
|
|
|
* To allow for future integration with the Contacts API and/ or potential
|
|
|
|
* integration with contact synchronization across devices (including Firefox OS
|
|
|
|
* devices), we are using objects with properties having the same names and
|
|
|
|
* structure as those used by mozContact.
|
|
|
|
*
|
|
|
|
* See https://developer.mozilla.org/en-US/docs/Web/API/mozContact for more
|
|
|
|
* information.
|
|
|
|
*/
|
|
|
|
const kFieldTypeString = "string";
|
|
|
|
const kFieldTypeNumber = "number";
|
|
|
|
const kFieldTypeNumberOrString = "number|string";
|
|
|
|
const kFieldTypeArray = "array";
|
|
|
|
const kFieldTypeBool = "boolean";
|
|
|
|
const kContactFields = {
|
|
|
|
"id": {
|
|
|
|
// Because "id" is externally generated, it might be numeric
|
|
|
|
type: kFieldTypeNumberOrString
|
|
|
|
},
|
|
|
|
"published": {
|
|
|
|
// mozContact, from which we are derived, defines dates as
|
|
|
|
// "a Date object, which will eventually be converted to a
|
|
|
|
// long long" -- to be forwards compatible, we allow both
|
|
|
|
// formats for now.
|
|
|
|
type: kFieldTypeNumberOrString
|
|
|
|
},
|
|
|
|
"updated": {
|
|
|
|
// mozContact, from which we are derived, defines dates as
|
|
|
|
// "a Date object, which will eventually be converted to a
|
|
|
|
// long long" -- to be forwards compatible, we allow both
|
|
|
|
// formats for now.
|
|
|
|
type: kFieldTypeNumberOrString
|
|
|
|
},
|
|
|
|
"bday": {
|
|
|
|
// mozContact, from which we are derived, defines dates as
|
|
|
|
// "a Date object, which will eventually be converted to a
|
|
|
|
// long long" -- to be forwards compatible, we allow both
|
|
|
|
// formats for now.
|
|
|
|
type: kFieldTypeNumberOrString
|
|
|
|
},
|
|
|
|
"blocked": {
|
|
|
|
type: kFieldTypeBool
|
|
|
|
},
|
|
|
|
"adr": {
|
|
|
|
type: kFieldTypeArray,
|
|
|
|
contains: {
|
|
|
|
"countryName": {
|
|
|
|
type: kFieldTypeString
|
|
|
|
},
|
|
|
|
"locality": {
|
|
|
|
type: kFieldTypeString
|
|
|
|
},
|
|
|
|
"postalCode": {
|
|
|
|
// In some (but not all) locations, postal codes can be strictly numeric
|
|
|
|
type: kFieldTypeNumberOrString
|
|
|
|
},
|
|
|
|
"pref": {
|
|
|
|
type: kFieldTypeBool
|
|
|
|
},
|
|
|
|
"region": {
|
|
|
|
type: kFieldTypeString
|
|
|
|
},
|
|
|
|
"streetAddress": {
|
|
|
|
type: kFieldTypeString
|
|
|
|
},
|
|
|
|
"type": {
|
|
|
|
type: kFieldTypeArray,
|
|
|
|
contains: kFieldTypeString
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"email": {
|
|
|
|
type: kFieldTypeArray,
|
|
|
|
contains: {
|
|
|
|
"pref": {
|
|
|
|
type: kFieldTypeBool
|
|
|
|
},
|
|
|
|
"type": {
|
|
|
|
type: kFieldTypeArray,
|
|
|
|
contains: kFieldTypeString
|
|
|
|
},
|
|
|
|
"value": {
|
|
|
|
type: kFieldTypeString
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"tel": {
|
|
|
|
type: kFieldTypeArray,
|
|
|
|
contains: {
|
|
|
|
"pref": {
|
|
|
|
type: kFieldTypeBool
|
|
|
|
},
|
|
|
|
"type": {
|
|
|
|
type: kFieldTypeArray,
|
|
|
|
contains: kFieldTypeString
|
|
|
|
},
|
|
|
|
"value": {
|
|
|
|
type: kFieldTypeString
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"name": {
|
|
|
|
type: kFieldTypeArray,
|
|
|
|
contains: kFieldTypeString
|
|
|
|
},
|
|
|
|
"honorificPrefix": {
|
|
|
|
type: kFieldTypeArray,
|
|
|
|
contains: kFieldTypeString
|
|
|
|
},
|
|
|
|
"givenName": {
|
|
|
|
type: kFieldTypeArray,
|
|
|
|
contains: kFieldTypeString
|
|
|
|
},
|
|
|
|
"additionalName": {
|
|
|
|
type: kFieldTypeArray,
|
|
|
|
contains: kFieldTypeString
|
|
|
|
},
|
|
|
|
"familyName": {
|
|
|
|
type: kFieldTypeArray,
|
|
|
|
contains: kFieldTypeString
|
|
|
|
},
|
|
|
|
"honorificSuffix": {
|
|
|
|
type: kFieldTypeArray,
|
|
|
|
contains: kFieldTypeString
|
|
|
|
},
|
|
|
|
"category": {
|
|
|
|
type: kFieldTypeArray,
|
|
|
|
contains: kFieldTypeString
|
|
|
|
},
|
|
|
|
"org": {
|
|
|
|
type: kFieldTypeArray,
|
|
|
|
contains: kFieldTypeString
|
|
|
|
},
|
|
|
|
"jobTitle": {
|
|
|
|
type: kFieldTypeArray,
|
|
|
|
contains: kFieldTypeString
|
|
|
|
},
|
|
|
|
"note": {
|
|
|
|
type: kFieldTypeArray,
|
|
|
|
contains: kFieldTypeString
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Compares the properties contained in an object to the definition as defined in
|
|
|
|
* `kContactFields`.
|
|
|
|
* If a property is encountered that is not found in the spec, an Error is thrown.
|
|
|
|
* If a property is encountered with an invalid value, an Error is thrown.
|
|
|
|
*
|
|
|
|
* Please read the spec at https://wiki.mozilla.org/Loop/Architecture/Address_Book
|
|
|
|
* for more information.
|
|
|
|
*
|
|
|
|
* @param {Object} obj The contact object, or part of it when called recursively
|
|
|
|
* @param {Object} def The definition of properties to validate against. Defaults
|
|
|
|
* to `kContactFields`
|
|
|
|
*/
|
|
|
|
const validateContact = function(obj, def = kContactFields) {
|
|
|
|
for (let propName of Object.getOwnPropertyNames(obj)) {
|
|
|
|
// Ignore internal properties.
|
|
|
|
if (propName.startsWith("_")) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
let propDef = def[propName];
|
|
|
|
if (!propDef) {
|
|
|
|
throw new Error("Field '" + propName + "' is not supported for contacts");
|
|
|
|
}
|
|
|
|
|
|
|
|
let val = obj[propName];
|
|
|
|
|
|
|
|
switch (propDef.type) {
|
|
|
|
case kFieldTypeString:
|
|
|
|
if (typeof val != kFieldTypeString) {
|
|
|
|
throw new Error("Field '" + propName + "' must be of type String");
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case kFieldTypeNumberOrString:
|
|
|
|
let type = typeof val;
|
|
|
|
if (type != kFieldTypeNumber && type != kFieldTypeString) {
|
|
|
|
throw new Error("Field '" + propName + "' must be of type Number or String");
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case kFieldTypeBool:
|
|
|
|
if (typeof val != kFieldTypeBool) {
|
|
|
|
throw new Error("Field '" + propName + "' must be of type Boolean");
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case kFieldTypeArray:
|
|
|
|
if (!Array.isArray(val)) {
|
|
|
|
throw new Error("Field '" + propName + "' must be an Array");
|
|
|
|
}
|
|
|
|
|
|
|
|
let contains = propDef.contains;
|
|
|
|
// If the type of `contains` is a scalar value, it means that the array
|
|
|
|
// consists of items of only that type.
|
|
|
|
let isScalarCheck = (typeof contains == kFieldTypeString);
|
|
|
|
for (let arrayValue of val) {
|
|
|
|
if (isScalarCheck) {
|
|
|
|
if (typeof arrayValue != contains) {
|
|
|
|
throw new Error("Field '" + propName + "' must be of type " + contains);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
validateContact(arrayValue, contains);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Provides a method to perform multiple operations in a single transaction on the
|
|
|
|
* contacts store.
|
|
|
|
*
|
|
|
|
* @param {String} operation Name of an operation supported by `IDBObjectStore`
|
|
|
|
* @param {Array} data List of objects that will be passed to the object
|
|
|
|
* store operation
|
|
|
|
* @param {Function} callback Function that will be invoked once the operations
|
|
|
|
* have finished. The first argument passed will be
|
|
|
|
* an `Error` object or `null`. The second argument
|
|
|
|
* will be the `data` Array, if all operations finished
|
|
|
|
* successfully.
|
|
|
|
*/
|
|
|
|
const batch = function(operation, data, callback) {
|
|
|
|
let processed = [];
|
|
|
|
if (!LoopContactsInternal.hasOwnProperty(operation) ||
|
|
|
|
typeof LoopContactsInternal[operation] != 'function') {
|
|
|
|
callback(new Error ("LoopContactsInternal does not contain a '" +
|
|
|
|
operation + "' method"));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
LoopStorage.asyncForEach(data, (item, next) => {
|
|
|
|
LoopContactsInternal[operation](item, (err, result) => {
|
|
|
|
if (err) {
|
|
|
|
next(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
processed.push(result);
|
|
|
|
next();
|
|
|
|
});
|
|
|
|
}, err => {
|
|
|
|
if (err) {
|
|
|
|
callback(err, processed);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
callback(null, processed);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Extend a `target` object with the properties defined in `source`.
|
|
|
|
*
|
|
|
|
* @param {Object} target The target object to receive properties defined in `source`
|
|
|
|
* @param {Object} source The source object to copy properties from
|
|
|
|
*/
|
|
|
|
const extend = function(target, source) {
|
|
|
|
for (let key of Object.getOwnPropertyNames(source)) {
|
|
|
|
target[key] = source[key];
|
|
|
|
}
|
|
|
|
return target;
|
|
|
|
};
|
|
|
|
|
|
|
|
LoopStorage.on("upgrade", function(e, db) {
|
|
|
|
if (db.objectStoreNames.contains(kObjectStoreName)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create the 'contacts' store as it doesn't exist yet.
|
|
|
|
let store = db.createObjectStore(kObjectStoreName, {
|
|
|
|
keyPath: kKeyPath,
|
|
|
|
autoIncrement: true
|
|
|
|
});
|
|
|
|
store.createIndex(kServiceIdIndex, kServiceIdIndex, {unique: false});
|
|
|
|
});
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The Contacts class.
|
|
|
|
*
|
|
|
|
* Each method that is a member of this class requires the last argument to be a
|
|
|
|
* callback Function. MozLoopAPI will cause things to break if this invariant is
|
|
|
|
* violated. You'll notice this as well in the documentation for each method.
|
|
|
|
*/
|
|
|
|
let LoopContactsInternal = Object.freeze({
|
2014-08-29 18:31:46 +00:00
|
|
|
/**
|
|
|
|
* Map of contact importer names to instances
|
|
|
|
*/
|
|
|
|
_importServices: {
|
|
|
|
"carddav": new CardDavImporter()
|
|
|
|
},
|
|
|
|
|
2014-08-12 10:24:07 +00:00
|
|
|
/**
|
|
|
|
* Add a contact to the data store.
|
|
|
|
*
|
|
|
|
* @param {Object} details An object that will be added to the data store
|
|
|
|
* as-is. Please read https://wiki.mozilla.org/Loop/Architecture/Address_Book
|
|
|
|
* for more information of this objects' structure
|
|
|
|
* @param {Function} callback Function that will be invoked once the operation
|
|
|
|
* finished. The first argument passed will be an
|
|
|
|
* `Error` object or `null`. The second argument will
|
|
|
|
* be the contact object, if it was stored successfully.
|
|
|
|
*/
|
|
|
|
add: function(details, callback) {
|
|
|
|
if (!(kServiceIdIndex in details)) {
|
|
|
|
callback(new Error("No '" + kServiceIdIndex + "' field present"));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
validateContact(details);
|
|
|
|
} catch (ex) {
|
|
|
|
callback(ex);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
LoopStorage.getStore(kObjectStoreName, (err, store) => {
|
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let contact = extend({}, details);
|
|
|
|
let now = Date.now();
|
|
|
|
// The data source should have included "published" and "updated" values
|
|
|
|
// for any imported records, and we need to keep track of those dated for
|
|
|
|
// sync purposes (i.e., when we add functionality to push local changes to
|
|
|
|
// a remote server from which we originally got a contact). We also need
|
|
|
|
// to track the time at which *we* added and most recently changed the
|
|
|
|
// contact, so as to determine whether the local or the remote store has
|
|
|
|
// fresher data.
|
|
|
|
//
|
|
|
|
// For clarity: the fields "published" and "updated" indicate when the
|
|
|
|
// *remote* data source published and updated the contact. The fields
|
|
|
|
// "_date_add" and "_date_lch" track when the *local* data source
|
|
|
|
// created and updated the contact.
|
|
|
|
contact.published = contact.published ? new Date(contact.published).getTime() : now;
|
|
|
|
contact.updated = contact.updated ? new Date(contact.updated).getTime() : now;
|
|
|
|
contact._date_add = contact._date_lch = now;
|
|
|
|
|
|
|
|
let request;
|
|
|
|
try {
|
|
|
|
request = store.add(contact);
|
|
|
|
} catch (ex) {
|
|
|
|
callback(ex);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
request.onsuccess = event => {
|
|
|
|
contact[kKeyPath] = event.target.result;
|
|
|
|
eventEmitter.emit("add", contact);
|
|
|
|
callback(null, contact);
|
|
|
|
};
|
|
|
|
|
|
|
|
request.onerror = event => callback(event.target.error);
|
|
|
|
}, "readwrite");
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add a batch of contacts to the data store.
|
|
|
|
*
|
|
|
|
* @param {Array} contacts A list of contact objects to be added
|
|
|
|
* @param {Function} callback Function that will be invoked once the operation
|
|
|
|
* finished. The first argument passed will be an
|
|
|
|
* `Error` object or `null`. The second argument will
|
|
|
|
* be the list of added contacts.
|
|
|
|
*/
|
|
|
|
addMany: function(contacts, callback) {
|
|
|
|
batch("add", contacts, callback);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove a contact from the data store.
|
|
|
|
*
|
|
|
|
* @param {String} guid String identifier of the contact to remove
|
|
|
|
* @param {Function} callback Function that will be invoked once the operation
|
|
|
|
* finished. The first argument passed will be an
|
|
|
|
* `Error` object or `null`. The second argument will
|
|
|
|
* be the result of the operation.
|
|
|
|
*/
|
|
|
|
remove: function(guid, callback) {
|
|
|
|
this.get(guid, (err, contact) => {
|
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
LoopStorage.getStore(kObjectStoreName, (err, store) => {
|
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let request;
|
|
|
|
try {
|
|
|
|
request = store.delete(guid);
|
|
|
|
} catch (ex) {
|
|
|
|
callback(ex);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
request.onsuccess = event => {
|
2014-09-01 20:44:51 +00:00
|
|
|
if (contact) {
|
|
|
|
eventEmitter.emit("remove", contact);
|
|
|
|
}
|
2014-08-12 10:24:07 +00:00
|
|
|
callback(null, event.target.result);
|
|
|
|
};
|
|
|
|
request.onerror = event => callback(event.target.error);
|
|
|
|
}, "readwrite");
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove a batch of contacts from the data store.
|
|
|
|
*
|
|
|
|
* @param {Array} guids A list of IDs of the contacts to remove
|
|
|
|
* @param {Function} callback Function that will be invoked once the operation
|
|
|
|
* finished. The first argument passed will be an
|
|
|
|
* `Error` object or `null`. The second argument will
|
|
|
|
* be the list of IDs, if successfull.
|
|
|
|
*/
|
|
|
|
removeMany: function(guids, callback) {
|
|
|
|
batch("remove", guids, callback);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove _all_ contacts from the data store.
|
|
|
|
* CAUTION: this method will clear the whole data store - you won't have any
|
|
|
|
* contacts left!
|
|
|
|
*
|
|
|
|
* @param {Function} callback Function that will be invoked once the operation
|
|
|
|
* finished. The first argument passed will be an
|
|
|
|
* `Error` object or `null`. The second argument will
|
|
|
|
* be the result of the operation, if successfull.
|
|
|
|
*/
|
|
|
|
removeAll: function(callback) {
|
|
|
|
LoopStorage.getStore(kObjectStoreName, (err, store) => {
|
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let request;
|
|
|
|
try {
|
|
|
|
request = store.clear();
|
|
|
|
} catch (ex) {
|
|
|
|
callback(ex);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
request.onsuccess = event => {
|
|
|
|
eventEmitter.emit("removeAll", event.target.result);
|
|
|
|
callback(null, event.target.result);
|
|
|
|
};
|
|
|
|
request.onerror = event => callback(event.target.error);
|
|
|
|
}, "readwrite");
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Retrieve a specific contact from the data store.
|
|
|
|
*
|
|
|
|
* @param {String} guid String identifier of the contact to retrieve
|
|
|
|
* @param {Function} callback Function that will be invoked once the operation
|
|
|
|
* finished. The first argument passed will be an
|
|
|
|
* `Error` object or `null`. The second argument will
|
|
|
|
* be the contact object, if successful.
|
2014-08-26 14:56:42 +00:00
|
|
|
* If no object matching guid could be found,
|
|
|
|
* then the callback is called with both arguments
|
|
|
|
* set to `null`.
|
2014-08-12 10:24:07 +00:00
|
|
|
*/
|
|
|
|
get: function(guid, callback) {
|
|
|
|
LoopStorage.getStore(kObjectStoreName, (err, store) => {
|
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let request;
|
|
|
|
try {
|
|
|
|
request = store.get(guid);
|
|
|
|
} catch (ex) {
|
|
|
|
callback(ex);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
request.onsuccess = event => {
|
|
|
|
if (!event.target.result) {
|
2014-08-26 14:56:42 +00:00
|
|
|
callback(null, null);
|
2014-08-12 10:24:07 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
let contact = extend({}, event.target.result);
|
|
|
|
contact[kKeyPath] = guid;
|
|
|
|
callback(null, contact);
|
|
|
|
};
|
|
|
|
request.onerror = event => callback(event.target.error);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Retrieve a specific contact from the data store using the kServiceIdIndex
|
|
|
|
* property.
|
|
|
|
*
|
|
|
|
* @param {String} serviceId String identifier of the contact to retrieve
|
|
|
|
* @param {Function} callback Function that will be invoked once the operation
|
|
|
|
* finished. The first argument passed will be an
|
|
|
|
* `Error` object or `null`. The second argument will
|
|
|
|
* be the contact object, if successfull.
|
2014-08-26 14:56:42 +00:00
|
|
|
* If no object matching serviceId could be found,
|
|
|
|
* then the callback is called with both arguments
|
|
|
|
* set to `null`.
|
2014-08-12 10:24:07 +00:00
|
|
|
*/
|
|
|
|
getByServiceId: function(serviceId, callback) {
|
|
|
|
LoopStorage.getStore(kObjectStoreName, (err, store) => {
|
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let index = store.index(kServiceIdIndex);
|
|
|
|
let request;
|
|
|
|
try {
|
|
|
|
request = index.get(serviceId);
|
|
|
|
} catch (ex) {
|
|
|
|
callback(ex);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
request.onsuccess = event => {
|
|
|
|
if (!event.target.result) {
|
2014-08-26 14:56:42 +00:00
|
|
|
callback(null, null);
|
2014-08-12 10:24:07 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let contact = extend({}, event.target.result);
|
|
|
|
callback(null, contact);
|
|
|
|
};
|
|
|
|
request.onerror = event => callback(event.target.error);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Retrieve _all_ contacts from the data store.
|
|
|
|
* CAUTION: If the amount of contacts is very large (say > 100000), this method
|
|
|
|
* may slow down your application!
|
|
|
|
*
|
|
|
|
* @param {Function} callback Function that will be invoked once the operation
|
|
|
|
* finished. The first argument passed will be an
|
|
|
|
* `Error` object or `null`. The second argument will
|
|
|
|
* be an `Array` of contact objects, if successfull.
|
|
|
|
*/
|
|
|
|
getAll: function(callback) {
|
|
|
|
LoopStorage.getStore(kObjectStoreName, (err, store) => {
|
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let cursorRequest = store.openCursor();
|
|
|
|
let contactsList = [];
|
|
|
|
|
|
|
|
cursorRequest.onsuccess = event => {
|
|
|
|
let cursor = event.target.result;
|
|
|
|
// No more results, return the list.
|
|
|
|
if (!cursor) {
|
|
|
|
callback(null, contactsList);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let contact = extend({}, cursor.value);
|
|
|
|
contact[kKeyPath] = cursor.key;
|
|
|
|
contactsList.push(contact);
|
|
|
|
|
|
|
|
cursor.continue();
|
|
|
|
};
|
|
|
|
|
|
|
|
cursorRequest.onerror = event => callback(event.target.error);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Retrieve an arbitrary amount of contacts from the data store.
|
|
|
|
* CAUTION: If the amount of contacts is very large (say > 1000), this method
|
|
|
|
* may slow down your application!
|
|
|
|
*
|
|
|
|
* @param {Array} guids List of contact IDs to retrieve contact objects of
|
|
|
|
* @param {Function} callback Function that will be invoked once the operation
|
|
|
|
* finished. The first argument passed will be an
|
|
|
|
* `Error` object or `null`. The second argument will
|
|
|
|
* be an `Array` of contact objects, if successfull.
|
|
|
|
*/
|
|
|
|
getMany: function(guids, callback) {
|
|
|
|
let contacts = [];
|
|
|
|
LoopStorage.asyncParallel(guids, (guid, next) => {
|
|
|
|
this.get(guid, (err, contact) => {
|
|
|
|
if (err) {
|
|
|
|
next(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
contacts.push(contact);
|
|
|
|
next();
|
|
|
|
});
|
|
|
|
}, err => {
|
|
|
|
callback(err, !err ? contacts : null);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update a specific contact in the data store.
|
|
|
|
* The contact object is modified by replacing the fields passed in the `details`
|
|
|
|
* param and any fields not passed in are left unchanged.
|
|
|
|
*
|
|
|
|
* @param {Object} details An object that will be updated in the data store
|
|
|
|
* as-is. Please read https://wiki.mozilla.org/Loop/Architecture/Address_Book
|
|
|
|
* for more information of this objects' structure
|
|
|
|
* @param {Function} callback Function that will be invoked once the operation
|
|
|
|
* finished. The first argument passed will be an
|
|
|
|
* `Error` object or `null`. The second argument will
|
|
|
|
* be the contact object, if successfull.
|
|
|
|
*/
|
|
|
|
update: function(details, callback) {
|
|
|
|
if (!(kKeyPath in details)) {
|
|
|
|
callback(new Error("No '" + kKeyPath + "' field present"));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
validateContact(details);
|
|
|
|
} catch (ex) {
|
|
|
|
callback(ex);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let guid = details[kKeyPath];
|
|
|
|
|
|
|
|
this.get(guid, (err, contact) => {
|
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2014-08-26 14:56:42 +00:00
|
|
|
if (!contact) {
|
|
|
|
callback(new Error("Contact with " + kKeyPath + " '" +
|
|
|
|
guid + "' could not be found"));
|
2014-09-01 20:44:51 +00:00
|
|
|
return;
|
2014-08-26 14:56:42 +00:00
|
|
|
}
|
|
|
|
|
2014-08-12 10:24:07 +00:00
|
|
|
LoopStorage.getStore(kObjectStoreName, (err, store) => {
|
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let previous = extend({}, contact);
|
|
|
|
// Update the contact with properties provided by `details`.
|
|
|
|
extend(contact, details);
|
|
|
|
|
|
|
|
details._date_lch = Date.now();
|
|
|
|
let request;
|
|
|
|
try {
|
|
|
|
request = store.put(contact);
|
|
|
|
} catch (ex) {
|
|
|
|
callback(ex);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
request.onsuccess = event => {
|
|
|
|
eventEmitter.emit("update", contact, previous);
|
|
|
|
callback(null, event.target.result);
|
|
|
|
};
|
|
|
|
request.onerror = event => callback(event.target.error);
|
|
|
|
}, "readwrite");
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Block a specific contact in the data store.
|
|
|
|
*
|
|
|
|
* @param {String} guid String identifier of the contact to block
|
|
|
|
* @param {Function} callback Function that will be invoked once the operation
|
|
|
|
* finished. The first argument passed will be an
|
|
|
|
* `Error` object or `null`. The second argument will
|
|
|
|
* be the contact object, if successfull.
|
|
|
|
*/
|
|
|
|
block: function(guid, callback) {
|
|
|
|
this.get(guid, (err, contact) => {
|
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2014-08-26 14:56:42 +00:00
|
|
|
if (!contact) {
|
|
|
|
callback(new Error("Contact with " + kKeyPath + " '" +
|
|
|
|
guid + "' could not be found"));
|
2014-09-01 20:44:51 +00:00
|
|
|
return;
|
2014-08-26 14:56:42 +00:00
|
|
|
}
|
|
|
|
|
2014-08-12 10:24:07 +00:00
|
|
|
contact.blocked = true;
|
|
|
|
this.update(contact, callback);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Un-block a specific contact in the data store.
|
|
|
|
*
|
|
|
|
* @param {String} guid String identifier of the contact to unblock
|
|
|
|
* @param {Function} callback Function that will be invoked once the operation
|
|
|
|
* finished. The first argument passed will be an
|
|
|
|
* `Error` object or `null`. The second argument will
|
|
|
|
* be the contact object, if successfull.
|
|
|
|
*/
|
|
|
|
unblock: function(guid, callback) {
|
|
|
|
this.get(guid, (err, contact) => {
|
|
|
|
if (err) {
|
|
|
|
callback(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2014-08-26 14:56:42 +00:00
|
|
|
if (!contact) {
|
|
|
|
callback(new Error("Contact with " + kKeyPath + " '" +
|
|
|
|
guid + "' could not be found"));
|
2014-09-01 20:44:51 +00:00
|
|
|
return;
|
2014-08-26 14:56:42 +00:00
|
|
|
}
|
|
|
|
|
2014-08-12 10:24:07 +00:00
|
|
|
contact.blocked = false;
|
|
|
|
this.update(contact, callback);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Import a list of (new) contacts from an external data source.
|
|
|
|
*
|
|
|
|
* @param {Object} options Property bag of options for the importer
|
|
|
|
* @param {Function} callback Function that will be invoked once the operation
|
|
|
|
* finished. The first argument passed will be an
|
|
|
|
* `Error` object or `null`. The second argument will
|
|
|
|
* be the result of the operation, if successfull.
|
|
|
|
*/
|
|
|
|
startImport: function(options, callback) {
|
2014-08-29 18:31:46 +00:00
|
|
|
if (!("service" in options)) {
|
|
|
|
callback(new Error("No import service specified in options"));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!(options.service in this._importServices)) {
|
|
|
|
callback(new Error("Unknown import service specified: " + options.service));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this._importServices[options.service].startImport(options, callback, this);
|
2014-08-12 10:24:07 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Search through the data store for contacts that match a certain (sub-)string.
|
|
|
|
*
|
|
|
|
* @param {String} query Needle to search for in our haystack of contacts
|
|
|
|
* @param {Function} callback Function that will be invoked once the operation
|
|
|
|
* finished. The first argument passed will be an
|
|
|
|
* `Error` object or `null`. The second argument will
|
|
|
|
* be an `Array` of contact objects, if successfull.
|
|
|
|
*/
|
|
|
|
search: function(query, callback) {
|
|
|
|
//TODO in bug 1037114.
|
|
|
|
callback(new Error("Not implemented yet!"));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Public Loop Contacts API.
|
|
|
|
*
|
|
|
|
* LoopContacts implements the EventEmitter interface by exposing three methods -
|
|
|
|
* `on`, `once` and `off` - to subscribe to events.
|
|
|
|
* At this point the following events may be subscribed to:
|
|
|
|
* - 'add': A new contact object was successfully added to the data store.
|
|
|
|
* - 'remove': A contact was successfully removed from the data store.
|
|
|
|
* - 'removeAll': All contacts were successfully removed from the data store.
|
|
|
|
* - 'update': A contact object was successfully updated with changed
|
|
|
|
* properties in the data store.
|
|
|
|
*/
|
|
|
|
this.LoopContacts = Object.freeze({
|
|
|
|
add: function(details, callback) {
|
|
|
|
return LoopContactsInternal.add(details, callback);
|
|
|
|
},
|
|
|
|
|
|
|
|
addMany: function(contacts, callback) {
|
|
|
|
return LoopContactsInternal.addMany(contacts, callback);
|
|
|
|
},
|
|
|
|
|
|
|
|
remove: function(guid, callback) {
|
|
|
|
return LoopContactsInternal.remove(guid, callback);
|
|
|
|
},
|
|
|
|
|
|
|
|
removeMany: function(guids, callback) {
|
|
|
|
return LoopContactsInternal.removeMany(guids, callback);
|
|
|
|
},
|
|
|
|
|
|
|
|
removeAll: function(callback) {
|
|
|
|
return LoopContactsInternal.removeAll(callback);
|
|
|
|
},
|
|
|
|
|
|
|
|
get: function(guid, callback) {
|
|
|
|
return LoopContactsInternal.get(guid, callback);
|
|
|
|
},
|
|
|
|
|
|
|
|
getByServiceId: function(serviceId, callback) {
|
|
|
|
return LoopContactsInternal.getByServiceId(serviceId, callback);
|
|
|
|
},
|
|
|
|
|
|
|
|
getAll: function(callback) {
|
|
|
|
return LoopContactsInternal.getAll(callback);
|
|
|
|
},
|
|
|
|
|
|
|
|
getMany: function(guids, callback) {
|
|
|
|
return LoopContactsInternal.getMany(guids, callback);
|
|
|
|
},
|
|
|
|
|
|
|
|
update: function(details, callback) {
|
|
|
|
return LoopContactsInternal.update(details, callback);
|
|
|
|
},
|
|
|
|
|
|
|
|
block: function(guid, callback) {
|
|
|
|
return LoopContactsInternal.block(guid, callback);
|
|
|
|
},
|
|
|
|
|
|
|
|
unblock: function(guid, callback) {
|
|
|
|
return LoopContactsInternal.unblock(guid, callback);
|
|
|
|
},
|
|
|
|
|
|
|
|
startImport: function(options, callback) {
|
|
|
|
return LoopContactsInternal.startImport(options, callback);
|
|
|
|
},
|
|
|
|
|
|
|
|
search: function(query, callback) {
|
|
|
|
return LoopContactsInternal.search(query, callback);
|
|
|
|
},
|
|
|
|
|
|
|
|
on: (...params) => eventEmitter.on(...params),
|
|
|
|
|
|
|
|
once: (...params) => eventEmitter.once(...params),
|
|
|
|
|
|
|
|
off: (...params) => eventEmitter.off(...params)
|
|
|
|
});
|