@codetanzania/ewea-event
Version:
A representation of an entity which define and track an instance(or occurrence) of an emergency(or disaster) event.
454 lines (407 loc) • 12.6 kB
JavaScript
import {
MODEL_NAME_EVENT,
MODEL_NAME_EVENTCHANGELOG,
COLLECTION_NAME_EVENT,
} from '@codetanzania/ewea-internals';
import { parallel, waterfall } from 'async';
import { forEach, get, includes, omit, pick, union } from 'lodash';
import { mergeObjects, idOf, join } from '@lykmapipo/common';
import { copyInstance, createSchema, model } from '@lykmapipo/mongoose-common';
import '@lykmapipo/mongoose-sequenceable';
import actions from 'mongoose-rest-actions';
import exportable from '@lykmapipo/mongoose-exportable';
import { Predefine } from '@lykmapipo/predefine';
import {
EVENT_SCHEMA_OPTIONS,
EVENT_STAGE_ALERT,
EVENT_STAGE_EVENT,
EVENT_STAGES,
EVENT_OPTION_SELECT,
EVENT_OPTION_AUTOPOPULATE,
EVENT_UPDATE_ARRAY_FIELDS,
EVENT_UPDATE_IGNORED_FIELDS,
EVENT_RELATION_PREDEFINE_FIELDS,
} from './internals';
import { sendEventUpdates, sendEventNotification } from './event.notifications';
import {
group,
type,
level,
severity,
certainty,
status,
urgency,
response,
} from './schema/base.schema';
import { location, address, areas } from './schema/geos.schema';
import { reporter, agencies, focals } from './schema/parties.schema';
import {
stage,
number,
causes,
description,
places,
instructions,
interventions,
impacts,
constraints,
remarks,
startedAt,
endedAt,
} from './schema/event.base.schema';
// TODO: calculate expose(risk) after create
// TODO: send actions after create
// TODO: ensure all fields in changelog schema?
// TODO: ensure administrative hierarchy
// TODO: ensure sources(name, email, link)
// TODO: provide impact summary
const SCHEMA = mergeObjects(
{ group, type, level, severity, certainty, status, urgency, response },
{ stage, number },
{ location, address },
{ causes, description, places },
{ areas },
{ reporter, agencies, focals },
{
instructions,
interventions,
impacts,
constraints,
remarks,
startedAt,
endedAt,
}
);
/**
* @module Event
* @namespace Event
* @name Event
* @description A representation of an entity which define and track an
* instance(or occurrence) of an emergency(or disaster) event.
*
* @see {@link https://en.wikipedia.org/wiki/Disaster}
*
* @author lally elias <lallyelias87@gmail.com>
* @license MIT
* @since 0.1.0
* @version 0.1.0
* @public
* @example
* const { Event } = require('@codetanzania/ewea-event');
* Event.create(event, (error, created) => { ... });
*/
const EventSchema = createSchema(
SCHEMA,
EVENT_SCHEMA_OPTIONS,
actions,
exportable
);
/*
*------------------------------------------------------------------------------
* Hooks
*------------------------------------------------------------------------------
*/
/**
* @name validate
* @function validate
* @description event 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
*/
EventSchema.pre('validate', function onPreValidate(done) {
return this.preValidate(done);
});
/*
*------------------------------------------------------------------------------
* Instance
*------------------------------------------------------------------------------
*/
/**
* @name preValidate
* @function preValidate
* @description event 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
*/
EventSchema.methods.preValidate = function preValidate(done) {
// ensure started(or reported) date
this.startedAt = this.startedAt || new Date();
// ensure group from type
if (this.type) {
this.group = get(this, 'type.relations.group', this.group);
}
return done(null, this);
};
/*
*------------------------------------------------------------------------------
* Statics
*------------------------------------------------------------------------------
*/
/* static constants */
EventSchema.statics.MODEL_NAME = MODEL_NAME_EVENT;
EventSchema.statics.COLLECTION_NAME = COLLECTION_NAME_EVENT;
EventSchema.statics.OPTION_SELECT = EVENT_OPTION_SELECT;
EventSchema.statics.OPTION_AUTOPOPULATE = EVENT_OPTION_AUTOPOPULATE;
EventSchema.statics.STAGE_ALERT = EVENT_STAGE_ALERT;
EventSchema.statics.STAGE_EVENT = EVENT_STAGE_EVENT;
EventSchema.statics.STAGES = EVENT_STAGES;
/**
* @name prepareSeedCriteria
* @function prepareSeedCriteria
* @description define seed data criteria
* @param {object} seed event to be seeded
* @returns {object} packed criteria for seeding
*
* @author lally elias <lallyelias87@gmail.com>
* @since 0.2.0
* @version 0.1.0
* @static
*/
EventSchema.statics.prepareSeedCriteria = (seed) => {
const copyOfSeed = copyInstance(seed);
const criteria = idOf(copyOfSeed)
? pick(copyOfSeed, '_id')
: pick(copyOfSeed, 'group', 'type', 'number');
return criteria;
};
/**
* @name preloadRelations
* @function preloadRelations
* @description Preload given event relations
* @param {object} event valid event instance or object
* @param {Function} done callback to invoke on success or error
* @returns {object|Error} pre-loaded relations or error
* @author lally elias <lallyelias87@gmail.com>
* @since 0.6.0
* @version 0.1.0
* @static
* @example
*
* const event = { _id: '...', group: '...', type: '...' };
* Event.preloadRelations(event, (error, updated) => { ... });
*/
EventSchema.statics.preloadRelations = (event, done) => {
// prepare relations to preload
const relations = {};
// prepare predefines loader
// for predefine use _id: $in
forEach(EVENT_RELATION_PREDEFINE_FIELDS, (criteria, relation) => {
const related = get(event, relation);
const relatedId = idOf(related) || related;
// event has relation
if (relatedId) {
relations[relation] = (next) =>
Predefine.getById({ _id: relatedId }, next);
}
// use default criteria
else if (criteria) {
relations[relation] = (next) => Predefine.findOne(criteria, next);
}
// continue
else {
relations[relation] = (next) => next(null, null);
}
});
// execute loader
return parallel(relations, done);
};
/**
* @name postWithChanges
* @function postWithChanges
* @description Update existing Event with the given changes
* @param {object} event valid event object to save
* @param {Function} done callback to invoke on success or error
* @returns {object|Error} created event or error
*
* @author lally elias <lallyelias87@gmail.com>
* @since 0.6.0
* @version 0.1.0
* @static
* @example
*
* const event = { type: '...', description: '...' };
* Event.postWithChanges(event, (error, created) => { ... });
*
*/
EventSchema.statics.postWithChanges = (event, done) => {
// ref
const Event = model(MODEL_NAME_EVENT);
// preload event relations
const preloadRelated = (next) => Event.preloadRelations(event, next);
// save event
const saveEvent = (relations, next) => {
const eventi = mergeObjects(event, relations);
return Event.post(eventi, next);
};
// send event notification
const sendNotification = (eventi, next) => {
// TODO: check AUTO_EVENT_NOTIFICATION_ENABLED=true
return sendEventNotification(eventi, (error /* , campaign */) => {
return next(error, eventi);
});
};
// TODO: ensure level, severity, certainty, status, urgency, response
// TODO: save initial changelog(reported)
// save event
const tasks = [preloadRelated, saveEvent, sendNotification];
return waterfall(tasks, done);
};
/**
* @name updateWith
* @function updateWith
* @description Update existing Event with the given criteria and changes
* @param {object} criteria valid event query criteria
* @param {object} changes valid event changes to apply
* @param {Function} done callback to invoke on success or error
* @returns {object|Error} updated event or error
*
* @author lally elias <lallyelias87@gmail.com>
* @since 0.6.0
* @version 0.1.0
* @static
* @example
*
* const criteria = { _id: '...' };
* const changes = { remarks: '...' };
* Event.updateWith(criteria, changes, (error, updated) => { ... });
*
*/
EventSchema.statics.updateWith = (criteria, changes, done) => {
// ref
const Event = model(MODEL_NAME_EVENT);
// find existing event by given criteria
const findEvent = (next) => {
return Event.findOne(criteria).orFail().exec(next);
};
// apply changes to found event
const applyChanges = (event, next) => {
// compute updates with ignores
const updates = { updatedAt: new Date() };
const allowedChanges = omit(changes, ...EVENT_UPDATE_IGNORED_FIELDS);
forEach(allowedChanges, (value, key) => {
const isArrayField = includes(EVENT_UPDATE_ARRAY_FIELDS, key);
// update array fields
if (isArrayField) {
const existValue = get(event, key);
updates[key] = union(existValue, [].concat(value));
}
// update simple fields
else {
updates[key] = value;
}
});
// persist event changes
event.set(updates);
return event.save(next);
};
// notify event updates
const sendUpdates = (event, next) => {
return sendEventUpdates(event, changes, (error /* , campaign */) => {
return next(error, event);
});
};
// update event
const tasks = [findEvent, applyChanges, sendUpdates];
return waterfall(tasks, done);
};
/**
* @name updateWithChanges
* @function updateWithChanges
* @description Update existing Event with the given changes
* @param {object} changes valid event changes to apply
* @param {Function} done callback to invoke on success or error
* @returns {object|Error} updated event or error
*
* @author lally elias <lallyelias87@gmail.com>
* @since 0.6.0
* @version 0.2.0
* @static
* @example
*
* const changes = { _id: '...', remarks: '...' };
* Event.updateWithChanges(criteria, changes, (error, updated) => { ... });
*
*/
EventSchema.statics.updateWithChanges = (changes, done) => {
// TODO: save all changes in `changes` path and make use: 'changes'
// ref
const Event = model(MODEL_NAME_EVENT);
const EventChangeLog = model(MODEL_NAME_EVENTCHANGELOG);
// obtain event id
const eventId = idOf(changes) || changes.id;
// post changelog
const postChangeLog = (next) => {
let changed = omit(changes, EVENT_UPDATE_IGNORED_FIELDS);
// TODO ensure event fields(description, instructions etc) in changelog
// TODO: pack as changes object
const comment = join(
[].concat(
changes.causes ||
changes.description ||
changes.places ||
changes.instructions ||
changes.interventions ||
changes.impacts ||
changes.constraints ||
changes.remarks
)
);
changed = mergeObjects(changed, { event: eventId, comment });
// TODO use EventChangeLog.postWithChanges once
// all event fields are also in changelog
return EventChangeLog.post(changed, next);
};
// update event with changes
const updateEvent = (changelog, next) => {
// TODO: track alert to event change time
const criteria = { _id: eventId };
return Event.updateWith(criteria, changes, next);
};
// update event with changes
const tasks = [postChangeLog, updateEvent];
return waterfall(tasks, done);
};
/**
* @name updateWithChangeLog
* @function updateWithChangeLog
* @description Update existing event with the given changelog instance
* @param {object} changelog valid event changelog to apply
* @param {Function} done callback to invoke on success or error
* @returns {object|Error} updated event or error
*
* @author lally elias <lallyelias87@gmail.com>
* @since 0.6.0
* @version 0.1.0
* @static
* @example
*
* const changes = { _id: '...', comment: '...' };
* Event.updateWithChangeLog(criteria, changes, (error, updated) => { ... });
*
*/
EventSchema.statics.updateWithChangeLog = (changelog, done) => {
// ref
const Event = model(MODEL_NAME_EVENT);
// return if changelog has no event
if (!changelog.event) {
return done(null, changelog);
}
// convert changelog to object
const changes = changelog.toObject();
// ensure event criteria
const criteria = { _id: idOf(changes.event) || changes.event };
// update event with changelog
return Event.updateWith(criteria, changes, done);
};
/* export event model */
export default model(MODEL_NAME_EVENT, EventSchema);