UNPKG

@codetanzania/emis-stakeholder

Version:

A representation of an entity (e.g municipal, individual, agency, organization etc) consisting of contact information (e.g. name, e-mail addresses, phone numbers) and other descriptive information of interest in emergency(or disaster) management.

1,796 lines (1,669 loc) 52.9 kB
import { compact, abbreviate, idOf, normalizeError, mergeObjects, firstValue, areNotEmpty, pkg } from '@lykmapipo/common'; import { getString, getStrings, getStringSet, apiVersion as apiVersion$1 } from '@lykmapipo/env'; import { model, Schema, ObjectId, SCHEMA_OPTIONS, copyInstance, toObjectIds, areSameInstance, connect } from '@lykmapipo/mongoose-common'; import { Router as Router$1, mount } from '@lykmapipo/express-common'; import { refresh, encode, jwtAuth as jwtAuth$1 } from '@lykmapipo/jwt-common'; import { Router, getFor, schemaFor, downloadFor, postFor, getByIdFor, patchFor, putFor, deleteFor, start as start$1 } from '@lykmapipo/express-rest-actions'; import { createModels } from '@lykmapipo/file'; import { permissionRouter } from '@lykmapipo/permission'; export { Permission, permissionRouter } from '@lykmapipo/permission'; import { Predefine, predefineRouter } from '@lykmapipo/predefine'; export { Predefine, predefineRouter } from '@lykmapipo/predefine'; import _, { isEmpty } from 'lodash'; import { parallel, waterfall } from 'async'; import irina from 'irina'; import actions from 'mongoose-rest-actions'; import exportable from '@lykmapipo/mongoose-exportable'; import { plugin } from 'mongoose-kue'; import { Email } from '@lykmapipo/postman'; import { Point } from 'mongoose-geojson-schemas'; import irinaUtils from 'irina/lib/utils'; import { parsePhoneNumber } from '@lykmapipo/phone'; /** * @module Party * @name Party * @description A representation of an entity (e.g municipal, individual, * agency, organization etc) consisting of contact information (e.g. name, * e-mail addresses, phone numbers) and other descriptive information of * interest in emergency(or disaster) management. * * It may be a self managed entity or division within another * entity(party) in case there is hierarchy. * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 0.1.0 * @version 0.1.0 * @public */ /* constants */ const POPULATION_MAX_DEPTH = 1; const PARTY_MODEL_NAME = getString('PARTY_MODEL_NAME', 'Party'); const PARTY_COLLECTION_NAME = getString('PARTY_COLLECTION_NAME', 'parties'); const DEFAULT_LOCALE = getString('DEFAULT_LOCALE', 'en'); const LOCALES = getStrings('LOCALES', DEFAULT_LOCALE); const DEFAULT_PARTY_TYPE = getString('DEFAULT_PARTY_TYPE', 'Focal'); const PARTY_TYPES = getStringSet('PARTY_TYPES', ['Focal', 'Agency']); const DEFAULT_PASSWORD = _.trim(getString('DEFAULT_PASSWORD', '123456789')); const OPTION_AUTOPOPULATE_GROUP = { select: { 'strings.name': 1, 'strings.abbreviation': 1, 'strings.description': 1, }, maxDepth: POPULATION_MAX_DEPTH, }; const OPTION_AUTOPOPULATE_ROLE = { select: { 'strings.name': 1, 'strings.abbreviation': 1, 'strings.description': 1, }, maxDepth: POPULATION_MAX_DEPTH, }; const OPTION_AUTOPOPULATE_OWNERSHIP = { select: { 'strings.name': 1, 'strings.abbreviation': 1, 'strings.description': 1, }, maxDepth: POPULATION_MAX_DEPTH, }; const OPTION_AUTOPOPULATE_GENDER = { select: { 'strings.name': 1, 'strings.abbreviation': 1, 'strings.description': 1, }, maxDepth: POPULATION_MAX_DEPTH, }; const OPTION_AUTOPOPULATE_AREA_LEVEL = { select: { 'strings.name': 1, 'strings.abbreviation': 1, 'strings.description': 1, }, maxDepth: POPULATION_MAX_DEPTH, }; const OPTION_AUTOPOPULATE_AREA = { select: { 'strings.name': 1, 'strings.color': 1, 'strings.code': 1, 'strings.abbreviation': 1, 'strings.description': 1, 'relations.level': 1, }, maxDepth: 2, }; const OPTION_AUTOPOPULATE = { select: { name: 1, email: 1, mobile: 1, abbreviation: 1 }, maxDepth: POPULATION_MAX_DEPTH, }; /** * @name PartySchema * @type {Schema} * @since 0.1.0 * @version 0.1.0 * @private */ const PartySchema = new Schema( { /** * @name party * @description Top party(i.e company, organization etc) under which a party * is derived(or member, part of etc). * * This is applicable where a large party delegates its power to its * division(s). * * If not set the party will be treated as a top party and will be affected * by any logics implemented accordingly. * @type {object} * @property {object} type - schema(data) type * @property {string} ref - referenced collection * @property {boolean} index - ensure database index * @property {boolean} exists - ensure ref exists before save * @property {object} autopopulate - auto population(eager loading) options * @property {boolean} taggable - allow field use for tagging * @since 0.1.0 * @version 0.1.0 * @instance * @example * { * _id: "5bcda2c073dd0700048fb846", * "type": "Agency", * name: "Bedfordshire", * phone: "+255715463739", * landline: "(886) 804-4219", * fax: "1-807-746-5438", * email: "zj.aj@ojwj.com", * } */ party: { type: ObjectId, ref: PARTY_MODEL_NAME, index: true, exists: true, aggregatable: { unwind: true }, autopopulate: OPTION_AUTOPOPULATE, taggable: true, }, /** * @name type * @description Human readable type of a party. * @type {object} * @property {object} type - schema(data) type * @property {boolean} trim - force trimming * @property {boolean} enum - list of acceptable values * @property {boolean} index - ensure database index * @property {boolean} searchable - allow for searching * @property {boolean} taggable - allow field use for tagging * @property {boolean} default - default value set when none provided * @property {object} fake - fake data generator options * @since 0.1.0 * @version 0.1.0 * @instance * @example * Agency */ type: { type: String, trim: true, enum: PARTY_TYPES, index: true, searchable: true, taggable: true, default: DEFAULT_PARTY_TYPE, fake: true, }, /** * @name group * @description Human readable group of a party. * @type {object} * @property {object} type - schema(data) type * @property {boolean} trim - force trimming * @property {boolean} enum - list of acceptable values * @property {boolean} index - ensure database index * @property {boolean} searchable - allow for searching * @property {boolean} taggable - allow field use for tagging * @property {boolean} default - default value set when none provided * @property {object} fake - fake data generator options * @since 0.1.0 * @version 0.1.0 * @instance * @example * Hospitals */ group: { type: ObjectId, ref: Predefine.MODEL_NAME, index: true, // required: true, exists: true, aggregatable: { unwind: true }, autopopulate: OPTION_AUTOPOPULATE_GROUP, taggable: true, exportable: { format: (v) => { return ( v && v.strings && compact([v.strings.name.en, v.strings.abbreviation.en]).join(' - ') ); }, order: 2, default: 'NA', }, default: undefined, }, /** * @name name * @description Human readable name of a party. * * It may be organization name e.g ACME Inc., person name e.g Juma John, * division withing organization e.g HR Dept etc. * @type {object} * @property {object} type - schema(data) type * @property {boolean} trim - force trimming * @property {boolean} required - mark required * @property {boolean} index - ensure database index * @property {boolean} searchable - allow for searching * @property {boolean} taggable - allow field use for tagging * @property {object} fake - fake data generator options * @since 0.1.0 * @version 0.1.0 * @instance * @example * ACME Inc. */ name: { type: String, trim: true, required: true, index: true, searchable: true, taggable: true, exportable: { order: 1 }, fake: { generator: 'name', type: 'findName', }, }, /** * @name abbreviation * @description Human readable short form of a party name. * @type {object} * @property {object} type - schema(data) type * @property {boolean} trim - force trimming * @property {boolean} uppercase - force upper-casing * @property {boolean} index - ensure database index * @property {boolean} searchable - allow for searching * @property {boolean} taggable - allow field use for tagging * @property {object} fake - fake data generator options * @since 0.1.0 * @version 0.1.0 * @instance * @example * ACME. */ abbreviation: { type: String, trim: true, uppercase: true, index: true, searchable: true, taggable: true, exportable: { order: 1 }, fake: { generator: 'hacker', type: 'abbreviation', }, }, /** * @name locale * @description Defines the party's language, region and any * special variant preferences. * @see {@link https://en.wikipedia.org/wiki/Locale_(computer_software)} * @type {object} * @property {object} type - schema(data) type * @property {boolean} trim - force trimming * @property {boolean} enum - list of acceptable values * @property {boolean} index - ensure database index * @property {boolean} searchable - allow for searching * @property {boolean} taggable - allow field use for tagging * @property {boolean} default - default value set when none provided * @property {object} fake - fake data generator options * @since 0.1.0 * @version 0.1.0 * @instance * @example * en, sw */ locale: { type: String, trim: true, enum: LOCALES, index: true, searchable: true, taggable: true, default: DEFAULT_LOCALE, fake: true, }, /** * @name email * @description Primary email address used to contact a party. * * Used when another party want to send direct mail to the other party. * @type {object} * @property {object} type - schema(data) type * @property {boolean} trim - force trimming * @property {boolean} required - mark required * @property {boolean} lowercase - force lower-casing * @property {boolean} email - force to be a valid email address * @property {boolean} index - ensure database index * @property {boolean} unique - ensure database unique index * @property {boolean} searchable - allow for searching * @property {boolean} taggable - allow field use for tagging * @property {object} fake - fake data generator options * @since 0.1.0 * @version 0.1.0 * @instance * @example * jow.joz@jottot.com */ email: { type: String, trim: true, required: true, lowercase: true, email: true, index: true, unique: true, searchable: true, taggable: true, exportable: { header: 'Email Address', order: 5 }, fake: { generator: 'internet', type: 'email', }, }, /** * @name mobile * @description Primary mobile phone number used to contact a party. * * Used when another party want to send a direct message or * call the other party. * @type {object} * @property {object} type - schema(data) type * @property {boolean} trim - force trimming * @property {boolean} required - mark required * @property {boolean} index - ensure database index * @property {boolean} unique - ensure database unique index * @property {boolean} searchable - allow for searching * @property {boolean} taggable - allow field use for tagging * @property {object} fake - fake data generator options * @since 0.1.0 * @version 0.1.0 * @instance * @example * 255765222333 */ mobile: { type: String, trim: true, required: true, phone: { e164: true, mobile: true, }, index: true, unique: true, searchable: true, taggable: true, exportable: { header: 'Mobile Number', order: 4 }, fake: (faker) => faker.helpers.replaceSymbolWithNumber('255714######'), }, /** * @name radio * @description Human readable radio call sign used to contact a party. * @type {object} * @property {object} type - schema(data) type * @property {boolean} trim - force trimming * @property {boolean} uppercase - force upper-casing * @property {boolean} index - ensure database index * @property {boolean} searchable - allow for searching * @property {boolean} taggable - allow field use for tagging * @property {object} fake - fake data generator options * @since 0.1.0 * @version 0.1.0 * @instance * @example * ACME. */ radio: { type: String, trim: true, uppercase: true, index: true, searchable: true, taggable: true, exportable: { header: 'Call Sign', order: 4 }, fake: { generator: 'commerce', type: 'color', }, }, /** * @name landline * @description Primary main-line(or fixed-line) phone number * used to contact a party. * * Used when another party want to direct call the other party. * @type {object} * @property {object} type - schema(data) type * @property {boolean} trim - force trimming * @property {boolean} index - ensure database index * @property {boolean} searchable - allow for searching * @property {boolean} taggable - allow field use for tagging * @property {object} fake - fake data generator options * @since 0.1.0 * @version 0.1.0 * @instance * @example * 255-242233642 */ landline: { type: String, trim: true, index: true, searchable: true, taggable: true, fake: (faker) => faker.helpers.replaceSymbolWithNumber('2552224#####'), }, /** * @name fax * @description Primary fax number used to contact a party. * @type {object} * @property {object} type - schema(data) type * @property {boolean} trim - force trimming * @property {boolean} index - ensure database index * @property {boolean} searchable - allow for searching * @property {boolean} taggable - allow field use for tagging * @property {object} fake - fake data generator options * @since 0.1.0 * @version 0.1.0 * @instance * @example * 255-242233642 */ fax: { type: String, trim: true, index: true, searchable: true, taggable: true, fake: (faker) => faker.helpers.replaceSymbolWithNumber('2552224#####'), }, /** * @name website * @description Primary website url(or link) of a party. * * Used when another party want to obtain specific information about * other party. * @type {object} * @property {object} type - schema(data) type * @property {boolean} trim - force trimming * @property {boolean} lowercase - force lower-casing * @property {boolean} index - ensure database index * @property {boolean} searchable - allow for searching * @property {boolean} taggable - allow field use for tagging * @property {object} fake - fake data generator options * @since 0.1.0 * @version 0.1.0 * @instance * @example * https://www.acme.com */ website: { type: String, trim: true, lowercase: true, index: true, searchable: true, taggable: true, fake: { generator: 'internet', type: 'url', }, }, /** * @name physicalAddress * @description Primary physical address of party office. * * Used when another party what to physical go or visit the other * party office. * @type {object} * @property {object} type - schema(data) type * @property {boolean} trim - force trimming * @property {boolean} index - ensure database index * @property {boolean} searchable - allow for searching * @property {boolean} taggable - allow field use for tagging * @property {object} fake - fake data generator options * @since 0.1.0 * @version 0.1.0 * @instance * @example * ACME Inc. * 2nd Floor "De Doctor Plaza" * Plot 440 Jomo Drive Masaki * Dar es Salaam, Tanzania */ physicalAddress: { type: String, trim: true, index: true, searchable: true, taggable: true, fake: { generator: 'address', type: 'streetAddress', }, }, /** * @name postalAddress * @description Primary postal address of party office. * * Used when another party what to send letter, percerls etc to anther * party office. * @type {object} * @property {object} type - schema(data) type * @property {boolean} trim - force trimming * @property {boolean} index - ensure database index * @property {boolean} searchable - allow for searching * @property {boolean} taggable - allow field use for tagging * @property {object} fake - fake data generator options * @since 0.1.0 * @version 0.1.0 * @instance * @example * ACME Inc. * P.O.Box 9683 * Dar es Salaam * Tanzania */ postalAddress: { type: String, trim: true, index: true, searchable: true, taggable: true, fake: { generator: 'address', type: 'streetAddress', }, }, /** * @name centre * @description A geo-location coordinates of a party main office or area of * operation. * * Its a coordinates(longitude and latidude) pair of office reachable by * other party or boundary of an area. * @type {object} * @property {object} location - geo json point * @property {string} location.type - Point * @property {number[]} location.coordinates - longitude, latitude pair of * the geo point * @since 0.1.0 * @version 0.1.0 * @instance * @example * { * type: 'Point', * coordinates: [-76.80207859497996, 55.69469494228919] * } */ centre: Point, /** * @name level * @description Administrative level of area of operation. * @type {object} * @property {object} type - schema(data) type * @property {string} ref - referenced model(or collection) * @property {boolean} index - ensure database index * @property {boolean} exists - ensure ref exists before save * @property {object} autopopulate - auto population(eager loading) options * @property {boolean} taggable - allow field use for tagging * @since 2.4.0 * @version 0.1.0 * @instance * @example * { * "name": {"en": "Region"} * } */ level: { type: ObjectId, ref: Predefine.MODEL_NAME, // required: true, index: true, exists: true, aggregatable: { unwind: true }, autopopulate: OPTION_AUTOPOPULATE_AREA_LEVEL, taggable: true, exportable: { header: 'Level', format: (v) => { return v && v.strings && compact([v.strings.name.en]).join(' - '); }, order: 3, default: 'NA', }, }, /** * @name area * @description Geographical location of a party main office or area of * operation. * @type {object} * @property {object} type - schema(data) type * @property {string} ref - referenced model(or collection) * @property {boolean} index - ensure database index * @property {boolean} exists - ensure ref exists before save * @property {object} autopopulate - auto population(eager loading) options * @property {boolean} taggable - allow field use for tagging * @since 1.1.0 * @version 0.1.0 * @instance * @example * { * "name": {"en": "Dar es Salaam"} * } */ area: { type: ObjectId, ref: Predefine.MODEL_NAME, // required: true, index: true, exists: true, aggregatable: { unwind: true }, autopopulate: OPTION_AUTOPOPULATE_AREA, taggable: true, exportable: { header: 'Area', format: (v) => { return v && v.strings && compact([v.strings.name.en]).join(' - '); }, order: 3, default: 'NA', }, }, /** * @name ownership * @description Assignable or given ownership to a party. * @type {object} * @property {object} type - schema(data) type * @property {string} ref - referenced collection * @property {boolean} index - ensure database index * @property {boolean} exists - ensure ref exists before save * @property {object} autopopulate - population options * @property {boolean} taggable - allow field use for tagging * @property {boolean} default - default value set when none provided * @since 2.6.0 * @version 0.1.0 * @instance * @example * { * "name": {"en": "Government"} * } */ ownership: { type: ObjectId, ref: Predefine.MODEL_NAME, index: true, // required: true, exists: true, aggregatable: { unwind: true }, autopopulate: OPTION_AUTOPOPULATE_OWNERSHIP, taggable: true, exportable: { header: 'Ownership', format: (v) => { return v && v.strings && compact([v.strings.name.en]).join(' - '); }, order: 2, default: 'NA', }, default: undefined, }, /** * @name role * @description Assignable or given role to a party. * @type {object} * @property {object} type - schema(data) type * @property {string} ref - referenced collection * @property {boolean} index - ensure database index * @property {boolean} exists - ensure ref exists before save * @property {object} autopopulate - population options * @property {boolean} taggable - allow field use for tagging * @property {boolean} default - default value set when none provided * @since 0.1.0 * @version 0.1.0 * @instance * @example * { * "name": {"en": "Administrator"} * } */ role: { type: ObjectId, ref: Predefine.MODEL_NAME, index: true, // required: true, exists: true, aggregatable: { unwind: true }, autopopulate: OPTION_AUTOPOPULATE_ROLE, taggable: true, exportable: { format: (v) => { return ( v && v.strings && compact([v.strings.name.en, v.strings.abbreviation.en]).join(' - ') ); }, order: 2, default: 'NA', }, default: undefined, }, /** * @name gender * @description Assignable or given gender to a party. * @type {object} * @property {object} type - schema(data) type * @property {string} ref - referenced collection * @property {boolean} index - ensure database index * @property {boolean} exists - ensure ref exists before save * @property {object} autopopulate - population options * @property {boolean} taggable - allow field use for tagging * @property {boolean} default - default value set when none provided * @since 2.6.0 * @version 0.1.0 * @instance * @example * { * "name": {"en": "Female"} * } */ gender: { type: ObjectId, ref: Predefine.MODEL_NAME, index: true, // required: true, exists: true, aggregatable: { unwind: true }, autopopulate: OPTION_AUTOPOPULATE_GENDER, taggable: true, exportable: { header: 'Gender', format: (v) => { return v && v.strings && compact([v.strings.name.en]).join(' - '); }, order: 2, default: 'NA', }, default: undefined, }, /** * @name token * @description Valid api access token for the party. * * Mainly used for parties that operate as client i.e mobile apps etc. * @type {object} * @property {object} type - schema(data) type * @property {boolean} trim - force trimming * @property {object} fake - fake data generator options * @since 2.2.0 * @version 0.1.0 * @instance */ token: { type: String, trim: true, }, }, SCHEMA_OPTIONS ); /* *------------------------------------------------------------------------------ * Hooks *------------------------------------------------------------------------------ */ /** * @name validate * @function validate * @description Party schema pre validation hook * @param {Function} done callback to invoke on success or error * @returns {object|Error} valid instance or error * @author lally elias <lallyelias87@gmail.com> * @since 0.1.0 * @version 0.1.0 * @private */ PartySchema.pre('validate', function onPreValidate(done) { return this.preValidate(done); }); /* *------------------------------------------------------------------------------ * Instance *------------------------------------------------------------------------------ */ /** * @name preValidate * @function preValidate * @description Party schema pre validation hook logic * @param {Function} done callback to invoke on success or error * @returns {object|Error} valid instance or error * @author lally elias <lallyelias87@gmail.com> * @since 0.1.0 * @version 0.1.0 * @instance */ PartySchema.methods.preValidate = function preValidate(done) { // ensure party abbreviation this.abbreviation = _.trim(this.abbreviation) || abbreviate(this.name); // extend party with default password // TODO: use hashed password if (_.isEmpty(this.password)) { this.password = DEFAULT_PASSWORD; } // ensure area level const level = _.get(this, 'area.relations.level'); if (!this.level && level) { this.level = level; } // ensure ownership const ownership = _.get(this, 'party.ownership'); if (!this.ownership && ownership) { this.ownership = ownership; } // ensure centre if (!this.centre && this.area && this.area.geos) { this.centre = this.area.geos.point; } // generate api token return this.generateToken(done); // TODO: set default group // TODO: set default level // TODO: set default area // TODO: set default ownership // TODO: set default role // TODO: set default gender // TODO: extract relations from parent if its (agency) }; /** * @name generateToken * @function generateToken * @description Generate party api token * @param {Function} done callback to invoke on success or error * @returns {object|Error} valid instance or error * @author lally elias <lallyelias87@gmail.com> * @since 0.1.0 * @version 0.1.0 * @instance */ PartySchema.methods.generateToken = function generateToken(done) { // refs const party = this; const expiresIn = getString('JWT_API_TOKEN_EXPIRES_IN', '1000y'); const payload = { id: idOf(party) }; const { token } = party; return refresh(token, payload, { expiresIn }, (error, jwtToken) => { if (error || !jwtToken) { const failed = normalizeError( error || new Error('Fail To Generate API Token'), { status: 500 } ); return done(failed); } party.token = jwtToken; return done(null, party); }); }; /** * @name asContact * @function asContact * @description Convert party to contact * @returns {object|Error} valid instance or error * @author lally elias <lallyelias87@gmail.com> * @since 2.2.0 * @version 0.1.0 * @instance */ PartySchema.methods.asContact = function asContact() { const contact = mergeObjects({ name: this.name, mobile: this.mobile, email: this.email, pushToken: this.pushToken, }); return contact; }; /* *------------------------------------------------------------------------------ * Statics *------------------------------------------------------------------------------ */ /* static constants */ PartySchema.statics.MODEL_NAME = PARTY_MODEL_NAME; PartySchema.statics.COLLECTION_NAME = PARTY_COLLECTION_NAME; PartySchema.statics.OPTION_AUTOPOPULATE = OPTION_AUTOPOPULATE; PartySchema.statics.DEFAULT_LOCALE = DEFAULT_LOCALE; PartySchema.statics.LOCALES = LOCALES; PartySchema.statics.DEFAULT_PARTY_TYPE = DEFAULT_PARTY_TYPE; PartySchema.statics.PARTY_TYPES = PARTY_TYPES; /** * @name prepareSeedCriteria * @function prepareSeedCriteria * @description Prepare party seeding upsert criteria * @param {object} seed plain object party seed * @returns {object} criteria used to upsert party * @author lally elias <lallyelias87@gmail.com> * @since 1.5.0 * @version 0.1.0 * @public */ PartySchema.statics.prepareSeedCriteria = (seed) => { const copyOfSeed = copyInstance(seed); const criteria = idOf(copyOfSeed) ? _.pick(copyOfSeed, '_id') : _.pick(copyOfSeed, 'name', 'email', 'mobile'); return criteria; }; /** * @name fetchContacts * @function fetchContacts * @description Obtain parties contacts based on specified criteria * @param {object} [criteria] valid query criteria * @param {Function} done a callback to invoke on success or error * @returns {object[] | Error} distinct contacts or error * @since 1.9.0 * @version 1.0.0 * @static */ PartySchema.statics.fetchContacts = function fetchContacts(criteria, done) { // refs const Party = this; // normalize arguments let conditions = _.isFunction(criteria) ? {} : _.merge({}, criteria); const cb = _.isFunction(criteria) ? criteria : done; // cast conditions conditions = Party.where(conditions).cast(Party); // execute fetch query return Party.find(conditions) .select({ name: 1, email: 1, mobile: 1 }) .exec(function onGetParties(error, parties) { let contacts; if (!error) { contacts = _.map([].concat(parties), function mapToContact(party) { return _.pick(party, 'name', 'email', 'mobile'); }); contacts = _.uniqWith(contacts, _.isEqual); } return cb(error, contacts); }); }; /** * @name getPhones * @function getPhones * @description pull distinct party phones * @param {object} [criteria] valid query criteria * @param {Function} done a callback to invoke on success or error * @returns {object[] | Error} distinct phones or error * @since 0.1.0 * @version 1.0.0 * @static */ PartySchema.statics.getPhones = function getPhones(criteria, done) { // refs const Party = this; // normalize arguments const copyOfcriteria = _.isFunction(criteria) ? {} : _.merge({}, criteria); const cb = _.isFunction(criteria) ? criteria : done; return Party.find(copyOfcriteria) .distinct('mobile') .exec(function onGetPhones(error, phones) { if (error) { return cb(error); } const copyOfPhones = _.uniq(_.compact([].concat(phones))); return cb(null, copyOfPhones); }); }; /** * @name getEmails * @function getEmails * @description pull distinct party emails * @param {object} [criteria] valid query criteria * @param {Function} done a callback to invoke on success or error * @returns {object[] | Error} distinct emails or error * @since 0.1.0 * @version 1.0.0 * @static */ PartySchema.statics.getEmails = function getEmails(criteria, done) { // refs const Party = this; // normalize arguments const copyOfcriteria = _.isFunction(criteria) ? {} : _.merge({}, criteria); const cb = _.isFunction(criteria) ? criteria : done; return Party.find(copyOfcriteria) .distinct('email') .exec(function onGetEmails(error, emails) { if (error) { return cb(error); } const copyOfEmails = _.uniq(_.compact([].concat(emails))); return cb(null, copyOfEmails); }); }; /** * @name notify * @function notify * @description send provide notification to parties * @param {object} notification valid notification payload * @param {Party} notification.to valid criteria to find party to notify * @param {string} notification.subject valid title for notification * @param {string} notification.body valid title for notification * @param {Function} done a callback to invoke on success or failure * @returns {object | Error} valid emails and phones or error * @since 0.1.0 * @version 0.1.0 * @instance */ PartySchema.statics.notify = function notify(notification, done) { // ref const Party = this; // ensure callback const cb = done || function noop() {}; // ensure notification const copyOfNotification = _.merge({ to: {} }, notification); // ensure valid notification payload const { to, subject, body } = copyOfNotification; const isValidNotification = !_.isEmpty(subject) || !_.isEmpty(body); if (!isValidNotification) { const error = new Error('Invalid Notification Payload'); error.status = 400; return cb(error); } // prepare receivers distinct emails and phones const works = {}; // query receivers phones works.phones = function getPartiesPhones(next) { const criteria = _.merge({}, to); Party.getPhones(criteria, next); }; // query receiver emails works.emails = function getPartiesEmails(next) { const criteria = _.merge({}, to); Party.getEmails(criteria, next); }; // query receivers return parallel(works, function onGetContacts(error, results) { // back off on error if (error) { return cb(error); } // collect email addresses let { emails } = results; emails = _.uniq(_.compact([].concat(emails))); // notify _.forEach(emails, function queueNotification(_to) { // prepare email payload const SMTP_FROM = getString('SMTP_FROM'); const mail = { sender: SMTP_FROM, to: _to, subject, body }; // queue emails const email = new Email(mail); email.queue(); }); // return return cb(null, results); }); }; /** * @name findByJwt * @function findByJwt * @description find existing party from jwt payload * @param {object} jwt valid jwt payload * @param {string} [jwt.id] valid party id * @param {Function} done a callback to invoke on success or error * @returns {object | Error} valid party * @since 0.2.0 * @version 0.1.0 * @static */ PartySchema.statics.findByJwt = function findByJwt(jwt, done) { // refs const Party = this; // prepare jwt error const jwtError = new Error('Invalid Authorization Token'); jwtError.status = 403; jwtError.code = 403; // find existing party const findPartyById = (next) => { if (_.isEmpty(jwt) || _.isEmpty(jwt.id)) { return next(jwtError); } return Party.findById(jwt.id, next); }; // ensure party exists and not blocked const verifyParty = (party, next) => { if (!party || party.deletedAt) { return next(jwtError); } return next(null, party); }; // execute return waterfall([findPartyById, verifyParty], done); }; /** * @name findDistinctAreas * @function findDistinctAreas * @description find distict areas from parties areas * @param {Function} done a callback to invoke on success or error * @returns {object[] | Error} distinct areas or error * @since 2.3.0 * @version 0.1.0 * @static */ PartySchema.statics.findDistinctAreas = function findDistinctAreas(done) { // refs const Party = this; // find distinct areas return Party.find() .setOptions({ autopopulate: false }) .select({ area: 1 }) .distinct('area') .lean() .exec(function onFindAreas(error, areas) { return done(error, areas); }); }; /** * @name findDistinctRoles * @function findDistinctRoles * @description find distict roles from parties roles * @param {Function} done a callback to invoke on success or error * @returns {object[] | Error} distinct roles or error * @since 2.3.0 * @version 0.1.0 * @static */ PartySchema.statics.findDistinctRoles = function findDistinctRoles(done) { // refs const Party = this; // find distinct roles return Party.find() .setOptions({ autopopulate: false }) .select({ role: 1 }) .distinct('role') .lean() .exec(function onFindRoles(error, roles) { return done(error, roles); }); }; /** * @name findDistinctGroups * @function findDistinctGroups * @description find distict groups from parties groups * @param {Function} done a callback to invoke on success or error * @returns {object[] | Error} distinct groups or error * @since 2.3.0 * @version 0.1.0 * @static */ PartySchema.statics.findDistinctGroups = function findDistinctGroups(done) { // refs const Party = this; // find distinct groups return Party.find() .setOptions({ autopopulate: false }) .select({ group: 1 }) .distinct('group') .lean() .exec(function onFindGroups(error, groups) { return done(error, groups); }); }; /** * @name findDistincts * @function findDistincts * @description find distict areas, roles and groups from parties * @param {Function} done a callback to invoke on success or error * @returns {object | Error} distict distict areas, roles and groups or error * @since 2.3.0 * @version 0.1.0 * @static */ PartySchema.statics.findDistincts = function findDistincts(done) { // refs const Party = this; // find distinct areas, roles & groups return parallel( { areas: (next) => Party.findDistinctAreas(next), roles: (next) => Party.findDistinctRoles(next), groups: (next) => Party.findDistinctGroups(next), }, done ); }; /** * @name findChildren * @function findChildren * @description Find party children recursively using given criteria * @param {object} criteria valid parent query options * @param {Function} done callback to invoke on success or error * @returns {object|Error} found parties or error * @author lally elias <lallyelias87@gmail.com> * @since 2.5.0 * @version 0.1.0 * @static * @example * * const criteria = { _id: ... }; * Party.findChildren(criteria, (error, results) => { ... }); * // => [ Party{ ... }, ... ] */ PartySchema.statics.findChildren = function findChildren(criteria, done) { // TODO: use $graphLookUp // ref const Party = this; let results = []; // collect results const collectResults = (...updates) => { let collected = _.uniq([...results, ...updates]); collected = _.uniqWith(collected, areSameInstance); return collected; }; // find children by their parents const findKids = (conditions, next) => { // prepare query const query = Party.find(conditions).setOptions({ autopopulate: false, }); // execute query return query.exec((error, children) => { // back-off on error if (error) { return next(error); } // continue find children if (!_.isEmpty(children)) { results = collectResults(...children); const parentIds = _.uniq(toObjectIds(...children)); if (_.isEmpty(parentIds)) { return next(null, results); } return findKids({ party: { $in: parentIds } }, next); } // continue return next(null, results); }); }; // do find recursively return findKids(criteria, done); }; /** * @name findParents * @function findParents * @description Find party parent recursively using given criteria * @param {object} criteria valid child query options * @param {Function} done callback to invoke on success or error * @returns {object|Error} found parties or error * @author lally elias <lallyelias87@gmail.com> * @since 2.5.0 * @version 0.1.0 * @static * @example * * const criteria = { _id: ... }; * Party.findParents(criteria, (error, results) => { ... }); * // => [ Party{ ... }, ... ] */ PartySchema.statics.findParents = function findParents(criteria, done) { // TODO: use $graphLookUp // ref const Party = this; let results = []; // collect results const collectResults = (...updates) => { let collected = _.uniq([...results, ...updates]); collected = _.uniqWith(collected, areSameInstance); return collected; }; // find parent by her children const findAncestors = (conditions, next) => { // prepare query const query = Party.find(conditions).setOptions({ autopopulate: false, }); // execute query return query.exec((error, ancestors) => { // back-off on error if (error) { return next(error); } // continue find parent if (!_.isEmpty(ancestors)) { results = collectResults(...ancestors); const ancestorIds = _.uniq(_.map(ancestors, 'party')); const parentIds = _.uniq(toObjectIds(...ancestorIds)); if (_.isEmpty(parentIds)) { return next(null, results); } return findAncestors({ _id: { $in: parentIds } }, next); } // continue return next(null, results); }); }; // do find recursively return findAncestors(criteria, done); }; /* *------------------------------------------------------------------------------ * Plugins *------------------------------------------------------------------------------ */ /* use mongoose rest actions */ PartySchema.plugin(irina, { registerable: { autoConfirm: true, }, }); PartySchema.plugin(actions); PartySchema.plugin(exportable); PartySchema.plugin(plugin); /* export party model */ var Party = model(PARTY_MODEL_NAME, PartySchema); /** * @name ensurePassword * @description Set plain password on party details * @param {object} party valid party details * @returns {object} party with plain password * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 3.0.0 * @version 1.0.0 * @public */ const ensurePassword$1 = (party = {}) => { const defaultPassword = getString('DEFAULT_PASSWORD', '123456789'); const password = firstValue(defaultPassword, party.password); const partyWithPassword = mergeObjects(party, { password }); return partyWithPassword; }; /** * @name encryptPassword * @description Encrypt party plain password * @param {object} party valid party details * @param {Function} done callback to invoke on success or failure * @returns {object} party with hashed password * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 3.0.0 * @version 1.0.0 * @public */ const encryptPassword$1 = (party = {}, done) => { const localParty = mergeObjects(party); if (!isEmpty(localParty.password)) { return irinaUtils.hash(localParty.password, 10, (error, hash) => { localParty.password = hash; return done(error, localParty); }); } return done(null, localParty); }; /** * @name ensurePassword * @description Ensure party password * @param {object} request valid http request * @param {object} response valid http response * @param {Function} next next middlware to invoke * @returns {Function} next middlware to invoke * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 0.1.0 * @version 1.0.0 * @public */ const ensurePassword = (request, response, next) => { if (!isEmpty(request.body)) { request.body = ensurePassword$1(request.body); return next(); } return next(); }; /** * @name encryptPassword * @description Encrypt party plain password * @param {object} request valid http request * @param {object} response valid http response * @param {Function} next next middlware to invoke * @returns {Function} next middlware to invoke * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 0.1.0 * @version 1.0.0 * @public */ const encryptPassword = (request, response, next) => { if (areNotEmpty(request.body, request.password)) { return encryptPassword$1(request.body, (error, party) => { if (error) { return next(error); } request.body = party; return next(); }); } return next(); }; /* constants */ const API_VERSION$1 = getString('API_VERSION', '1.0.0'); const PATH_SINGLE = '/parties/:id'; const PATH_LIST = '/parties'; const PATH_CHILDREN = '/parties/:party/parties'; const PATH_SCHEMA = '/parties/schema'; const PATH_EXPORT = '/parties/export'; const PATH_NOTIFICATION = '/notifications'; /** * @name PartyHttpRouter * @namespace PartyHttpRouter * @description A representation of an entity (e.g municipal, individual, * agency, organization etc) consisting of contact information (e.g. name, * e-mail addresses, phone numbers) and other descriptive information of * interest in emergency(or disaster) management. * @author lally elias <lallyelias87@gmail.com> * @license MIT * @since 0.1.0 * @version 1.0.0 * @public */ const router$1 = new Router({ version: API_VERSION$1, }); /** * @name GetParties * @memberof PartyHttpRouter * @description Returns a list of parties */ router$1.get( PATH_LIST, getFor({ get: (options, done) => Party.get(options, done), }) ); /** * @name GetPartySchema * @memberof PartyHttpRouter * @description Returns party json schema definition */ router$1.get( PATH_SCHEMA, schemaFor({ getSchema: (query, done) => { const jsonSchema = Party.jsonSchema(); return done(null, jsonSchema); }, }) ); /** * @name ExportParties * @memberof PartyHttpRouter * @description Export parties as csv */ router$1.get( PATH_EXPORT, downloadFor({ download: (options, done) => { const fileName = `parties_exports_${Date.now()}.csv`; const readStream = Party.exportCsv(options); return done(null, { fileName, readStream }); }, }) ); /** * @name PostParty * @memberof PartyHttpRouter * @description Create new party */ router$1.post( PATH_LIST, ensurePassword, postFor({ post: (body, done) => Party.register(body, done), }) ); /** * @name GetParty * @memberof PartyHttpRouter * @description Get existing party */ router$1.get( PATH_SINGLE, getByIdFor({ getById: (options, done) => Party.getById(options, done), }) ); /** * @name PatchParty * @memberof PartyHttpRouter * @description Patch existing party */ router$1.patch( PATH_SINGLE, encryptPassword, patchFor({ patch: (options, done) => Party.patch(options, done), }) ); /** * @name PutParty * @memberof PartyHttpRouter * @description Put existing party */ router$1.put( PATH_SINGLE, encryptPassword, putFor({ put: (options, done) => Party.put(options, done), }) ); /** * @name DeleteParty * @memberof PartyHttpRouter * @description Delete existing party */ router$1.delete( PATH_SINGLE, deleteFor({ del: (options, done) => Party.del(options, done), soft: true, }) ); /** * @name GetSubParties * @memberof PartyHttpRouter * @description Returns a list of sub-parties */ router$1.get( PATH_CHILDREN, getFor({ get: (options, done) => Party.get(options, done), }) ); /** * @name PostNotification * @name GetSubParties * @memberof PartyHttpRouter * @description Send new notification to parties * @example * * POST /notifications */ router$1.post( PATH_NOTIFICATION, postFor({ post: (body, done) => Party.notify(body, done), }) ); /* constants */ const API_VERSION = getString('API_VERSION', '1.0.0'); const router = new Router$1({ version: API_VERSION, }); /** * @description A route to handle authentication/signin to the application * @version 0.2.0 * @since 1.5.0 */ router.post('/signin', (request, response, next) => { /* prevent invalid sign in credentials */ if ( _.isEmpty(request.body) || _.isEmpty(request.body.username) || _.isEmpty(request.body.password) ) { next(new Error('Invalid signin details')); } // continue with signin else { // normalize credentials const { username } = _.merge({}, request.body); const email = username.toLowerCase(); const parsedPhoneNumber = parsePhoneNumber(username); let phone; // check if phone number was parsed if (parsedPhoneNumber) { phone = parsedPhoneNumber.e164NoPlus; } const credentials = { $or: [{ email }, { mobile: phone || username }], deletedAt: { $eq: null, }, password: request.body.password, }; waterfall( [ function authenticateParty(then) { // authenticate active party only Party.authenticate(credentials, then); }, // ensure roles & permissions function populate(party, then) { party.populate('role', then); }, function encodePartyToJWT(party, then) { encode({ id: idOf(party) }, function afterEncode(error, jwtToken) { if (error) { then(error); } else { then(null, { party: _.merge(party.toJSON()), token: jwtToken, }); } }); }, ], function done(error, result) { // fail to authenticate party // return error message if (error) { // Set forbidden status code normalizeError({ status: 403 }); next(error); } // party authenticated successfully // token generated successfully else { response.ok({ success: true, party: result.party, token: result.token, }); } } ); } }); /** * @module Party * @namespace Party * @name Party * @description A representation of an entity (e.g municipal, individual, * agency, organization etc) consisting of contact information (e.g. name,