VulpesOS/gaia/apps/calendar/js/backend/store/account.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

400 lines
9.6 KiB
JavaScript
Executable file

define(function(require, exports, module) {
'use strict';
var AccountModel = require('models/account');
var Abstract = require('./abstract');
var core = require('core');
var debug = require('common/debug')('store/account');
var denodeifyAll = require('common/promise').denodeifyAll;
var extend = require('common/extend');
var nextTick = require('common/next_tick');
var probablyParseInt = require('common/probably_parse_int');
function Account() {
Abstract.apply(this, arguments);
denodeifyAll(this, [
'verifyAndPersist',
'sync',
'markWithError',
'syncableAccounts',
'availablePresets'
]);
}
module.exports = Account;
Account.prototype = {
__proto__: Abstract.prototype,
_store: 'accounts',
_parseId: probablyParseInt,
/**
* Checks if a given account is a duplicate of another.
*
* @param {Calendar.Model.Account} model to check.
* @param {Function} callback [err].
*/
_validateModel: function(model, callback) {
this.all(function(err, allAccounts) {
if (err) {
callback(err);
return;
}
// check if this account is already registered
for (var index in allAccounts) {
if (
allAccounts[index].user === model.user &&
allAccounts[index].fullUrl === model.fullUrl &&
allAccounts[index]._id !== model._id
) {
var dupErr = new Error(
'Cannot add two accounts with the same url / entry point'
);
dupErr.name = 'account-exist';
callback(dupErr);
return;
}
}
callback();
});
},
verifyAndPersist: function(model, callback) {
var self = this;
var provider = core.providerFactory.get(
model.providerType
);
provider.getAccount(model.toJSON(), function(err, data) {
if (err) {
callback(err);
return;
}
model.error = undefined;
// if this works we always will get a calendar home.
// This is used to find calendars.
model.calendarHome = data.calendarHome;
// server may override properties on demand.
extend(model, data);
self._validateModel(model, function(err) {
if (err) {
return callback(err);
}
self.persist(model, callback);
});
});
},
/**
* Because this is a top-level store
* when we remove an account all records
* related to it must be removed.
*/
_dependentStores: [
'accounts', 'calendars', 'events',
'busytimes', 'alarms', 'icalComponents'
],
_removeDependents: function(id, trans) {
var store = core.storeFactory.get('Calendar');
store.remotesByAccount(id, trans, function(err, related) {
if (err) {
return console.error('Error removing deps for account: ', id);
}
var key;
for (key in related) {
store.remove(related[key]._id, trans);
}
});
},
/**
* Syncs all calendars for account.
*
* TODO: Deprecate this method in favor of new provider API's.
*
* @param {Calendar.Models.Account} account sync target.
* @param {Function} callback node style.
*/
sync: function(account, callback) {
//TODO: We need to block removal when syncing
//OR after removal ensure everything created here
//is purged.
var self = this;
var provider = core.providerFactory.get(account.providerType);
var calendarStore = core.storeFactory.get('Calendar');
var persist = [];
// remotesByAccount return an object indexed by remote ids
var calendars;
// these are remote ids not local ones
var originalIds;
function fetchExistingCalendars(err, results) {
if (err) {
return callback(err);
}
calendars = results;
originalIds = Object.keys(calendars);
provider.findCalendars(account, persistCalendars);
}
function persistCalendars(err, remoteCals) {
var key;
if (err) {
callback(err);
return;
}
for (key in remoteCals) {
if (remoteCals.hasOwnProperty(key)) {
var cal = remoteCals[key];
var idx = originalIds.indexOf(key);
if (idx !== -1) {
// update an existing calendar
originalIds.splice(idx, 1);
var original = calendars[key];
original.remote = cal;
original.error = undefined;
persist.push(original);
} else {
// create a new calendar
persist.push(
calendarStore._createModel({
remote: new Object(cal),
accountId: account._id
})
);
}
}
}
// at this point whatever is left in originalIds
// is considered a removed calendar.
// update / remove
if (persist.length || originalIds.length) {
var trans = core.db.transaction(
self._dependentStores,
'readwrite'
);
originalIds.forEach(function(id) {
calendarStore.remove(calendars[id]._id, trans);
});
persist.forEach(function(object) {
calendarStore.persist(object, trans);
});
// event listeners must come at the end
// because persist/remove also listen to
// transaction complete events.
trans.addEventListener('error', function(err) {
callback(err);
});
trans.addEventListener('complete', function() {
callback(null);
});
} else {
// invoke callback nothing to sync
callback(null);
}
}
calendarStore.remotesByAccount(
account._id,
fetchExistingCalendars
);
},
_createModel: function(obj, id) {
if (!(obj instanceof AccountModel)) {
obj = new AccountModel(obj);
}
if (typeof(id) !== 'undefined') {
obj._id = id;
}
return obj;
},
/**
* Marks given model with an error and sends an error event with the given
* model
*
* This will trigger an 'error' event immediately with the given model.
* The callback fires _after_ the event. Its entirely possible (under rare
* conditions) that this operation will fail but the event will fire.
*
*
* @param {Object} account model.
* @param {Calendar.Error} error to mark model with.
* @param {IDBTransaction} [trans] optional transaction.
* @param {Function} [callback] optional called with [err, model].
*/
markWithError: function(account, error, trans, callback) {
if (typeof(trans) === 'function') {
callback = trans;
trans = null;
}
if (!account._id) {
throw new Error('given account must be persisted');
}
if (!account.error) {
account.error = {
name: error.name,
date: new Date(),
count: 0
};
}
// increment the error count
account.error.count++;
var calendarStore = core.storeFactory.get('Calendar');
var self = this;
function fetchedCalendars(err, calendars) {
if (!trans) {
trans = core.db.transaction(
self._dependentStores,
'readwrite'
);
}
if (err) {
console.error('Cannot fetch all calendars', err);
return self.persist(account, trans, callback);
}
for (var id in calendars) {
calendarStore.markWithError(calendars[id], error, trans);
}
self.persist(account, trans);
self._transactionCallback(trans, callback);
}
// find related calendars and mark those too
calendarStore.remotesByAccount(
account._id,
fetchedCalendars
);
},
/**
* Finds and returns all accounts that can sync (based on their provider).
*
* accountStore.syncableAccounts(function(err, list) {
* if (list.length === 0)
* // hide sync options
* });
*
* @param {Function} callback [Error err, Array accountList].
*/
syncableAccounts: function(callback) {
debug('Will find syncable accounts...');
this.all((err, list) => {
if (err) {
return callback(err);
}
var results = [];
for (var key in list) {
var account = list[key];
var provider = core.providerFactory.get(account.providerType);
if (provider.canSync) {
results.push(account);
}
}
callback(null, results);
});
},
/**
* Returns a list of available presets filtered by
* the currently used presets in the database.
*
* Expected structure of the presetList is as follows:
*
* {
* 'presetType': {
* // most important field when true if the preset
* // is available in the database that preset type
* // will be excluded.
* singleUse: true
* providerType: 'X',
* options: {}
* }
*
* }
*
* @param {Object} presetList see example ^^^.
* @param {Function} callback [err, ['presetKey', ...]].
*/
availablePresets: function(presetList, callback) {
var results = [];
var singleUse = {};
var hasSingleUses = false;
for (var preset in presetList) {
if (presetList[preset].singleUse) {
hasSingleUses = true;
singleUse[preset] = true;
} else {
results.push(preset);
}
}
if (!hasSingleUses) {
return nextTick(function() {
callback(null, results);
});
}
this.all(function(err, list) {
if (err) {
callback(err);
return;
}
for (var id in list) {
var preset = list[id].preset;
if (singleUse[preset]) {
delete singleUse[preset];
}
}
// add un-used presets to the list.
callback(null, results.concat(Object.keys(singleUse)));
});
}
};
});