ldsorg
Version:
A JavaScript library for accessing LDS.org with Node.js or a browser
727 lines (640 loc) • 21.2 kB
JavaScript
/*jshint -W054 */
;(function (exports) {
'use strict';
var forEachAsync = exports.forEachAsync || require('foreachasync').forEachAsync
;
function LdsOrg(opts) {
var me = this
;
if (!(me instanceof LdsOrg)) {
return new LdsOrg(opts);
}
opts = opts || {};
if (opts.node) {
require('./node').LdsOrgNode.init(LdsOrg, ldsOrgP);
} else if (opts.phantom) {
require('./phantom').init(LdsOrg, ldsOrgP);
} else {
(exports.LdsOrgBrowser || require('./browser').LdsOrgBrowser).init(LdsOrg, ldsOrgP);
}
me._prefetch = opts.prefetch;
me._Cache = opts.Cache;
me._cacheOpts = opts.cacheOpts || {};
me._promises = {};
}
LdsOrg.create = LdsOrg;
LdsOrg.toArray = function (mapOrArr) {
if (!Array.isArray(mapOrArr) && 'object' === typeof mapOrArr) {
mapOrArr = Object.keys(mapOrArr).map(function (key) {
return mapOrArr[key];
});
}
return mapOrArr;
};
// LdsOrg.mapIds(55555, 'wardUnitNo') // => { wardUnitNo: 55555 }
LdsOrg.mapId = function (objOrId, idName) {
var obj = {}
;
if ('object' === typeof objOrId) {
//return objOrId;
obj[idName] = objOrId[idName];
return obj;
}
// assume string
obj[idName] = objOrId;
return obj;
};
// LdsOrg.mapIds([55555], 'wardUnitNo') // => [{ wardUnitNo: 55555 }]
LdsOrg.mapIds = function (array, name) {
/*
if ('object' === typeof array[0]) {
return array;
}
*/
return array.map(function (element) {
var obj = {}
;
//obj[name] = element;
obj[name] = LdsOrg.mapId(element, name)[name];
return obj;
});
};
LdsOrg._events = [
"cacheInit"
, "cacheReady"
, "meta"
//, "area"
, "stakeInit"
, "stakeCallingsInit"
, "stakePositionsInit"
, "stakePositions"
, "stakeLeadershipInit"
, "stakeLeadership"
, "stakeCallings"
, "stake"
// gets wards
, "stakeEnd"
, "wardInit"
, "wardCallingsInit"
, "wardPositionsInit"
, "wardPositions"
, "wardLeadershipInit"
, "wardLeadership"
, "wardCallings"
, "wardOrganizationsInit"
, "wardOrganizationInit"
, "wardOrganization"
, "wardOrganizations"
, "householdsInit"
, "households"
// gets households
, "wardEnd"
, "householdInit"
, "household"
, "householdPhotoInit"
, "householdPhoto"
, "individualPhotoInit"
, "individualPhoto"
, "householdEnd"
];
var defaultKeepAlive = 1 * 24 * 60 * 60 * 1000
, LdsOrgWard = (exports.LdsOrgWard || require('./ward').LdsOrgWard)
, LdsOrgStake = (exports.LdsOrgStake || require('./stake').LdsOrgStake)
, LdsStake = LdsOrgStake.init(LdsOrg, LdsOrgWard.init(LdsOrg))
, ldsOrgP = LdsOrg.prototype
, Join = exports.Join || require('join').Join
;
LdsOrg._urls = {};
LdsOrg._urls.base = 'https://www.lds.org/directory/services/ludrs';
//
// Session Cacheable
//
// URLs
LdsOrg._urls.currentStake = '/unit/current-user-units/';
LdsOrg.getCurrentStakeUrl = function () {
return LdsOrg._urls.base + LdsOrg._urls.currentStake;
};
LdsOrg._urls.currentMeta = '/unit/current-user-ward-stake/';
LdsOrg.getCurrentMetaUrl = function () {
return LdsOrg._urls.base + LdsOrg._urls.currentMeta;
};
LdsOrg._urls.currentUserInfo = '/mem/current-user-info/';
LdsOrg.getCurrentUserInfoUrl = function () {
return LdsOrg._urls.base + LdsOrg._urls.currentUserInfo;
};
//
// Ward Cacheable
// (shared between users)
//
// URLs
LdsOrg._urls.household = '/mem/householdProfile/{{household_id}}';
// https://www.lds.org/directory/services/ludrs/mem/householdProfile/:head_of_house_individual_id
LdsOrg.getHouseholdUrl = function (householdId) {
return (LdsOrg._urls.base
+ LdsOrg._urls.household
.replace(/{{household_id}}/g, householdId)
);
};
LdsOrg._urls.wardLeadershipPositions = "/1.1/unit/ward-leadership-positions/{{ward_unit_no}}/true";
// https://www.lds.org/directory/services/ludrs/1.1/unit/ward-leadership-positions/:ward_unit_no/true
LdsOrg.getWardLeadershipPositionsUrl = function (wardUnitNo) {
return (LdsOrg._urls.base
+ LdsOrg._urls.wardLeadershipPositions
.replace(/{{ward_unit_no}}/g, wardUnitNo)
);
};
LdsOrg._urls.wardLeadershipGroup = "/1.1/unit/stake-leadership-group-detail/{{ward_unit_no}}/{{group_key}}/{{instance}}";
// https://www.lds.org/directory/services/ludrs/1.1/unit/stake-leadership-group-detail/:ward_unit_no/:group_key/:instance
LdsOrg.getWardLeadershipGroupUrl = function (wardUnitNo, groupKey, instance) {
return (LdsOrg._urls.base
+ LdsOrg._urls.wardLeadershipGroup
.replace(/{{ward_unit_no}}/g, wardUnitNo)
.replace(/{{group_key}}/g, groupKey)
.replace(/{{instance}}/g, instance)
);
};
LdsOrg._urls.wardOrganization = "/1.1/unit/roster/{{ward_unit_no}}/{{organization}}";
// https://www.lds.org/directory/services/ludrs/1.1/unit/roster/:ward_unit_no/:organization
LdsOrg.getWardOrganizationUrl = function (wardUnitNo, organization) {
return (LdsOrg._urls.base
+ LdsOrg._urls.wardOrganization
.replace(/{{ward_unit_no}}/g, wardUnitNo)
.replace(/{{organization}}/g, organization)
);
};
LdsOrg._urls.memberList = '/mem/member-list/';
// https://www.lds.org/directory/services/ludrs/mem/member-list/:ward_unit_number
LdsOrg.getMemberListUrl = function (wardUnitNo) {
return LdsOrg._urls.base + LdsOrg._urls.memberList + wardUnitNo;
};
LdsOrg._urls.photos = '/mem/wardDirectory/photos/';
// https://www.lds.org/directory/services/ludrs/mem/wardDirectory/photos/:ward_unit_number
LdsOrg.getPhotosUrl = function (wardUnitNo) {
return LdsOrg._urls.base + LdsOrg._urls.photos + wardUnitNo;
};
LdsOrg._urls.individualPhotos = '/photo/url/{{individual_ids}}/individual';
LdsOrg.getIndividualPhotosUrl = function (individualIds) {
return (LdsOrg._urls.base
+ LdsOrg._urls.individualPhotos
.replace(/{{individual_ids}}/g, individualIds.join(','))
);
};
//
// Stake Cacheable
//
// URLs
LdsOrg._urls.stakeLeadershipPositions = "/1.1/unit/stake-leadership-positions/{{stake_unit_no}}";
// https://www.lds.org/directory/services/ludrs/1.1/unit/stake-leadership-positions/:stake_unit_no
LdsOrg.getStakeLeadershipPositionsUrl = function (stakeUnitNo) {
return (LdsOrg._urls.base
+ LdsOrg._urls.stakeLeadershipPositions
.replace(/{{stake_unit_no}}/g, stakeUnitNo)
);
};
LdsOrg._urls.stakeLeadershipGroup = "/1.1/unit/stake-leadership-group-detail/{{stake_unit_no}}/{{group_key}}/{{instance}}";
// https://www.lds.org/directory/services/ludrs/1.1/unit/stake-leadership-group-detail/:ward_unit_no/:group_key/:instance
LdsOrg.getStakeLeadershipGroupUrl = function (stakeUnitNo, groupKey, instance) {
return (LdsOrg._urls.base
+ LdsOrg._urls.stakeLeadershipGroup
.replace(/{{stake_unit_no}}/g, stakeUnitNo)
.replace(/{{group_key}}/g, groupKey)
.replace(/{{instance}}/g, instance)
);
};
// TODO move lateral here
// TODO abstract requests??
// get cb, abstractUrl, { individualId: 123456 }, { cacheable: "cache://pic/:id", contentType: 'image' }
// maybe use String.supplant?
LdsOrg._getJSON = function (cb, opts) {
//opts = opts || {};
// opts.cacheId
// opts.cache
// opts.stake
// opts.ward
// opts.makeRequest
// opts.store
// opts.ldsOrg
function respondWithCache(err, data) {
var stale = true
// , useless // TODO determine if it's way too stale
, promiseId = opts.cacheId || opts.url
;
// respond with the old data, but queue up new data in the background
if (data) {
stale = (Date.now() - data.updatedAt) < (opts.keepAlive || defaultKeepAlive);
if (!(opts.noCache || opts.expire)) {
cb(null, data.value);
cb = function () {};
}
if (!stale) {
return;
}
}
// poor-man's promises
if (opts.ldsOrg._promises[promiseId]) {
opts.ldsOrg._promises[promiseId].push(cb);
return;
} else {
opts.ldsOrg._promises[promiseId] = [cb];
}
opts.ldsOrg.makeRequest(function (err, _data) {
if (_data && opts.cacheId && !opts.noCache) {
var obj = { _id: opts.cacheId, updatedAt: Date.now(), value: _data };
obj._rev = (_data || {})._rev;
opts.store.put(function () {}, opts.cacheId, obj);
}
opts.ldsOrg._promises[promiseId].forEach(function (cb) {
cb(err, _data);
});
delete opts.ldsOrg._promises[promiseId];
}, opts.url);
}
// TODO cache here by url
// TODO assume base
if (opts.cacheId) {
opts.store.get(respondWithCache, opts.cacheId);
} else {
respondWithCache();
}
};
LdsOrg._getImage = function (cb, opts) {
function respondWithCache(err, data) {
var stale = true
// , useless // TODO determine if it's way too stale
, promiseId = opts.cacheId || opts.url
;
// respond with the old data, but queue up new data in the background
if (data) {
stale = (Date.now() - data.updatedAt) < (opts.keepAlive || defaultKeepAlive);
if (!(opts.noCache || opts.expire)) {
cb(null, data.value);
cb = function () {};
}
if (!stale) {
return;
}
}
// poor-man's promises
if (opts.ldsOrg._promises[promiseId]) {
opts.ldsOrg._promises[promiseId].push(cb);
return;
} else {
opts.ldsOrg._promises[promiseId] = [cb];
}
opts.ldsOrg.getImageData(function (err, _data) {
if (_data && opts.cacheId && !opts.noCache) {
var obj = { _id: opts.cacheId, updatedAt: Date.now(), value: _data };
obj._rev = (_data || {})._rev;
opts.store.put(function () {}, opts.cacheId, obj);
}
opts.ldsOrg._promises[promiseId].forEach(function (cb) {
cb(err, _data);
});
delete opts.ldsOrg._promises[promiseId];
}, opts.url);
}
// TODO cache here by url
// TODO assume base
opts.store.get(respondWithCache, opts.cacheId);
};
// Organizations
LdsOrg._organizations = [
"HIGH_PRIEST"
, "ELDER"
, "RELIEF_SOCIETY"
, "PRIEST"
, "TEACHER"
, "DEACON"
, "LAUREL"
, "MIA_MAID"
, "BEEHIVE"
, "ADULTS" // the lone plural organization
];
ldsOrgP.init = function (cb, eventer) {
var me = this
, cacheOpts = {}
;
me.areas = {};
me.wards = {};
me.stakes = {};
me.homeArea = {};
me.homeAreaStakes = {};
me.homeStakeWards = {};
Object.keys(me._cacheOpts).forEach(function (key) {
cacheOpts[key] = me._cacheOpts[key];
});
cacheOpts.ldsOrg = me;
me._emit = eventer || function () {};
me._emit('cacheInit');
me._store = me._Cache.create(cacheOpts, cacheOpts);
me._store.init(function () {
me._emit('cacheReady');
me.getCurrentUserMeta(cb);
});
};
ldsOrgP.signin = function (cb, auth) {
var me = this
, guestRe
;
me._auth = me._auth || auth;
guestRe = /^(gandalf|dumbledore|test|guest|demo|aoeu|asdf|root|admin|secret|pass|password|12345678|anonymous)$/i;
if (!me._auth || !me._auth.username) {
cb(new Error("You didn't specify a username."));
return;
}
if (this._hogwarts || (guestRe.test(me._auth.username) && guestRe.test(me._auth.password))) {
if (exports.window) {
exports.window.alert("You are using a demo account. Welcome to Hogwarts!");
}
if (!this._hogwarts) {
console.info('Welcome to Hogwarts! ;-)');
}
this._hogwarts = true;
cb(null);
return;
}
me._signin(function (err, data) {
if (!err) {
me._authenticated = Date.now();
}
cb(err, data);
}, me._auth);
};
ldsOrgP.signout = function (cb) {
var me = this
;
this._hogwarts = false;
ldsOrgP._signout(function (err) {
if (!err) {
me._authenticated = 0;
}
cb(err);
});
};
ldsOrgP.makeRequest = function (cb, url) {
var me = this
, count = 0
;
function doItNow() {
me._makeRequest(function (err, data) {
if (err && count < 2) {
console.error('Request Failed:', url);
console.error(err);
count += 1;
me.signin(doItNow);
return;
}
cb(err, data);
}, url);
}
if (this._hogwarts) {
if (!this._hogwartsBackend) {
this._hogwartsBackend = (exports.Hogwarts || require('./hogwarts').Hogwarts).create();
require('fs').writeFileSync('./hogwarts.cache', JSON.stringify(this._hogwartsBackend.cache, null, ' '));
}
me._makeRequest = this._hogwartsBackend.makeRequest;
}
// I think the lds.org timeout is about 15 to 30 minutes of inactivity, not entirely sure
// It would take about 1.5 hrs to download a complete area
// at 25s per ward with 11 wards per stake and 18 stakes
if (!me._authenticated || (Date.now() - me._authenticated > 30 * 60 * 1000)) {
me.signin(doItNow, me._auth);
} else {
doItNow();
}
};
ldsOrgP.getImageData = function (next, imgSrc) {
var me = this
;
if (this._hogwarts) {
this._getImageData(next, imgSrc);
return;
}
function doItNow() {
me._getImageData(next, 'https://www.lds.org' + imgSrc);
}
if (!me._authenticated) {
// Note that the photo path would have probably expired by this time... but whatevs
me._signin(doItNow);
} else {
doItNow();
}
};
// Methods
ldsOrgP.getCurrentUserId = function (fn) {
var me = this
;
me.getCurrentUserInfo(function (userInfo) {
fn(userInfo.individualId);
});
};
ldsOrgP.getCurrentUserInfo = function (fn) {
var me = this
;
LdsOrg._getJSON(
function (err, data) {
me._currentUserId = data.individualId;
me._emit('currentUserId', data.individualInfo);
me._currentUserInfo = data;
me._emit('currentUserInfo', data);
fn(data);
}
, { url: LdsOrg.getCurrentUserInfoUrl()
, store: me._store
, cacheId: 'current-user-info'
, ldsOrg: me
}
);
};
ldsOrgP.getCurrentUnits = function (fn) {
var me = this
;
LdsOrg._getJSON(
function (err, units) {
me._currentUnits = units;
me._emit('currentUnits', units);
fn(units);
}
, { url: LdsOrg.getCurrentMetaUrl()
, store: me._store
, cacheId: 'current-units'
, ldsOrg: me
}
);
};
ldsOrgP.getCurrentStakes = function (fn) {
var me = this
;
LdsOrg._getJSON(
function (err, stakeList) {
me._currentStakes = stakeList;
me._emit('currentStakes', stakeList);
fn(stakeList);
}
, { url: LdsOrg.getCurrentStakeUrl()
, store: me._store
, cacheId: 'current-stakes'
, ldsOrg: me
}
);
};
//
// Session Composite
//
ldsOrgP.getCurrentUserMeta = function (fn) {
var me = this
//, areaInfoId = 'area-info'
//, stakesInfoId = 'stakes-info'
, join = Join.create()
, userJ = join.add()
, metaJ = join.add()
, stakeJ = join.add()
;
me.getCurrentUserInfo(function (userInfo) {
me.currentUserId = userInfo.individualId;
me.currentUserInfo = userInfo;
userJ(null, userInfo.individualId, userInfo);
});
me.getCurrentUnits(function (units) {
me._areaMeta = units;
me.homeAreaId = units.areaUnitNo;
me.homeStakeId = units.stakeUnitNo;
me.homeWardId = units.wardUnitNo;
me.homeArea.areaUnitNo = units.areaUnitNo;
metaJ(null, units);
});
me.getCurrentStakes(function (stakes) {
me.homeArea.stakes = stakes;
me.homeArea.stakes.forEach(function (stake) {
me.homeAreaStakes[stake.stakeUnitNo] = stake;
me.stakes[stake.stakeUnitNo] = stake;
});
// TODO me._emit('area', me.homeArea);
Object.keys(me.stakes).forEach(function (stakeNo) {
var stake = me.stakes[stakeNo]
;
stake.wards.forEach(function (ward) {
me.wards[ward.wardUnitNo] = ward;
});
});
stakeJ(null, stakes);
});
join.then(function (userArgs, unitArgs, stakeArgs) {
var meta
;
meta = {
currentUserId: userArgs[1]
, currentUserInfo: userArgs[2]
, currentUnits: unitArgs[1]
, currentStakes: stakeArgs[1]
// oh which key to use... dummy, fake, mock, anonymous, guest, test, demo?
// or... real, authentic, verified, validated
// accountType, role (no), entityType
// hmmmm... guest sounds very polite
// but requiring true to be present is a better practice than requiring false to be absent
// but real sounds stupid and one day there may also be a 'verified'... and guest sounds nice
// but best practices... well, 'guest' is intuitively obvious, 'real' is not
, guest: true === me._hogwarts
};
me.homeStake = me.stakes[me.homeStakeId];
me.homeStake.wards.forEach(function (ward) {
me.homeStakeWards[ward.wardUnitNo] = ward;
});
me.homeWard = me.wards[me.homeWardId];
me.getCurrentHousehold(function (household) {
meta.currentHousehold = household;
// Just a lookin' for ways to speed things up...
// here's the tough part: you can't get email addresses without getting the household,
// and since the pictures expire quickly and they can also only be gotton from household data
// it seems that there's no quick way to get all ward data so we might as well get pics
// or also skip emails...
if (me._prefetch) {
// for a stake of 12 wards it took 1m 5s
// an area of 18 stakes would get done in about 20 min
me.prefetchArea(function () {
// add in the email addresses and pictures and you're up another hour or so
me.prefetchArea(function () {
// nada
}, { fullHouseholds: true });
}, { fullHouseholds: false });
}
me._emit('meta', meta);
fn(meta);
});
});
};
ldsOrgP.prefetchStake = function (cb, stakeUnitNo, opts) {
var me = this
, bigData = { wards: [] }
;
me.getStake(stakeUnitNo).getAll(function (stakeData) {
bigData.stake = stakeData;
me.getStake(stakeUnitNo).getCurrentWard().getAll(function (currentWardData) {
var stake
;
bigData.wards.push(currentWardData);
stake = me.stakes[me.homeStakeId];
forEachAsync(stake.wards, function (next, ward) {
if (me.homeWardId === ward.wardUnitNo) {
next();
return;
}
me.getCurrentStake().getWard(ward.wardUnitNo).getAll(function (wardData) {
bigData.wards.push(wardData);
next();
}, opts);
}).then(function () {
// FYI it would be stupid to try to transmit this to a browser in a single request
cb(bigData);
});
}, opts);
}, opts);
};
ldsOrgP.prefetchArea = function (cb, opts) {
var me = this
, areaData = { stakes: [] }
;
me.prefetchStake(function (stake) {
areaData.stakes.push(stake);
forEachAsync(me.homeArea.stakes, function (next, stake) {
if (me.homeStakeId === stake.stakeUnitNo) {
next();
return;
}
me.prefetchStake(function (stakeData) {
areaData.stakes.push(stakeData);
next();
}, stake.stakeUnitNo, opts);
}).then(function () {
// FYI it would be beyond stupid to try to transmit this to a browser as a single request
cb(areaData);
});
}, me.homeStakeId, opts);
};
ldsOrgP.getCurrentHousehold = function (fn, opts) {
opts = opts || {};
var me = this
;
me.getCurrentStake().getCurrentWard().getHouseholdWithPhotos(fn, me.currentUserId, opts);
};
ldsOrgP.getUserMeta = ldsOrgP.getCurrentUserMeta;
ldsOrgP.getStake = function (stakeUnitNo) {
var me = this
;
me._realStakes = me._realStakes || {};
if (!me._realStakes[stakeUnitNo]) {
me._realStakes[stakeUnitNo] = LdsStake.create({ stakeUnitNo: stakeUnitNo }, me);
me._realStakes[stakeUnitNo].init(function () {});
}
return me._realStakes[stakeUnitNo];
};
ldsOrgP.getCurrentStake = function () {
var me = this
;
return me.getStake(me.homeStakeId);
};
exports.LdsOrg = LdsOrg.LdsOrg = LdsOrg;
if ('undefined' !== typeof module) {
module.exports = exports.LdsOrg;
}
}('undefined' !== typeof exports && exports || new Function('return this')()));