src/services/FlowService.js
import { deprecate } from 'core-decorators';
import Client from '../Client';
import Flow from '../helpers/flow/Flow';
import FlowKeyValue from '../helpers/flow/FlowKeyValue';
import JsonApiService from './JsonApiService';
import {
hasAttribute,
hasToken,
isA,
isRequired,
isValid,
validate,
} from '../utilities/ValidationUtility';
// import FlowKeyValue from '../helpers/flow/FlowKeyValue';
/**
* Service handling Flow process calls with specific endpoints.
*
* The Clinical6 server can create flows or multi page forms that enable the colleciton of data.
* The SDK allows you to retrieve the information stored about each flow process so that it can be
* integrated into the app.
*/
class FlowService extends JsonApiService {
/**
* Update type to be data_collection__captured_value_groups
*/
constructor() {
super();
/** @type {String} - The type of the default data */
// this.type = 'data_collection__captured_value_groups';
this.type = 'data_collection__flow_processes';
}
/**
* Collects data from the flow process via a id.
*
* Should really use the {@link Flow} methods instead that call this method.
*
* @throws {Clinical6Error} - If missing token or missing required parameters
* @param {!Flow} flow - Flow process to which collect from
* @param {!String} flow.id - The permanent link for the requested flow process.
* @param {?Object[]} flow.fields - Collected data from users
* @param {?Object} [options] - Various options to change the way data is saved.
* @return {Promise} - Promise object that returns success or failure
*
* @example
* import { flowService } from 'clinical6';
* flowService.getFlow('favorite_flow').then(flow => {
* flowService.collect(flow); // not the best way to do this.
* flow.collect(); // this is the way to do this
* });
*/
collect(flow, options = {}) {
validate('FlowService.collect',
hasToken(),
isRequired({ flow }),
hasAttribute({ flow }, 'id'),
hasAttribute({ flow }, '_fields'));
return this.collectFields(flow, flow.fields, options);
}
/**
* Collects data outside the flow process. This is used for per step saves.
*
* Should really use the {@link Flow} methods instead that call this method.
*
* @throws {Clinical6Error} - If missing token or missing required parameters
* @param {!Flow} flow - Flow process to which collect from
* @param {!String} flow.id - The permanent link for the requested flow process.
* @param {!Object[]} fields - Collected data from users
* @return {Promise} - Promise object that returns success or failure
*/
collectFields(flow, fields) {
validate('FlowService.collectFields',
hasToken(),
isRequired({ flow }),
hasAttribute({ flow }, 'id'));
const attributes = {};
fields.forEach(field => (attributes[field.input_id] = field.value));
const params = {
data: {
type: 'data_collection__flow_process_values',
attributes,
relationships: {
flow_process: {
data: {
id: flow.id, // number
type: 'data_collection__flow_processes'
}
},
owner: {
data: {
id: flow.owner || Client.instance.user.id,
type: flow.ownerType
}
}
}
}
};
if (flow.flowDataGroupId) {
params.data.relationships.captured_value_group = {
data: {
id: flow.flowDataGroupId,
type: 'data_collection__captured_value_groups'
}
};
}
// const fkv = new FlowKeyValue(params);
// fields.forEach(field => (fkv[field.input_id] = field.value));
// fkv.flow = flow;
// fkv.owner = flow.owner;
// if (flow.flowDataGroup) {
// fkv.flowDataGroup = flow.flowDataGroup;
// }
let promise;
if (flow.entry) {
promise = Client.instance.fetch(`/v3/ediary/entries/${flow.entry.id}/data_collection/flow_process_values`, 'post', params);
} else {
params.data.relationships.owner.data.type = (flow.ownerType === 'mobile_user') ? 'mobile_users' : flow.ownerType;
const fkv = new FlowKeyValue(params);
promise = fkv.save().then((result) => {
if (result.flowDataGroup && result.flowDataGroup.id) {
flow.flowDataGroupId = result.flowDataGroup.id;
}
return result;
});
}
return promise;
}
/**
* Delete (soft delete / hide) a flow process value resource given an id
*
* http://clinical6-docs.s3-website-us-east-1.amazonaws.com/apidoc/v3data_collectionflow_process_values/destroy.html
*
* @throws {Clinical6Error} - If missing token or missing required parameters
* @param {!Object} keyValue - The process value object used to save information
* @param {!Number} keyValue.id - The flow id, must be a number
* @param {?String} [cacheMode] - A string to indicate how this is cached.
* @return {Promise<any>} - Promise object that returns success or failure
*
* @example
* import { FlowKeyValue, flowService } from 'clinical6';
*
* const v = new FlowKeyValue({ id: 5 });
* const response = await flowService.deleteKeyValue(v);
*
* @example
* import { flowService } from 'clinical6';
*
* const response = await flowService.deleteKeyValue({ id: 5 });
*/
async deleteKeyValue(keyValue, cacheMode = undefined) {
const errorTitle = 'FlowService.deleteKeyValue';
validate(errorTitle,
hasToken(),
isRequired({ keyValue }),
hasAttribute({ keyValue }, 'id'));
if (!keyValue.type) {
keyValue.type = 'data_collection__flow_process_values';
}
return super.delete(keyValue, { cacheMode });
}
/**
* @override
* Call a GET request expecting JSON API information.
*
* @throws {Clinical6Error} - If missing token or missing required parameters
* @param {Object} [params] - Parameters used to get information from server (such as id)
* @param {Number} [params.id] - Id of the flow
* @param {Number} [params.permanentLink] - permanentLink of the flow
* @param {String} [cacheMode] - Override the caching method for this call
* @return {Promise<Flow[] | Flow>} - Promise object that returns one flow or an array
*
* @example
* import { flowService } from 'clinical6';
*
* // You will be able to access these flows using the `get` method.
* flowService.get().then(flows => console.log(flows));
*
* // Additionally, you can retrieve a specific flow with the `get` method, using the permanent link of the desired flow
* // as a parameter.
* flowService.get({ permanentLink: 'favorite_flow' }).then(flow => console.log(flow));
*/
get(params = {}, cacheMode = undefined) {
validate('FlowService.get',
hasToken());
return (params.permanentLink) ? this.get(params.permanentLink) : super.get(params, { cacheMode });
}
/**
* Gets the flow process from a id.
*
* The best way to pull back a flow process is by using the id or resource provided by the server.
* To get a collection of Flows or Sections which may contain Flows is using the
* {@link StatusService#getSections} method. This will automatically call the
* {@link Section#connectChildren} method if it has children which calls the
* {@link Section#getData} method calling this method ({@link FlowService#getFlow}).
*
* @throws {Clinical6Error} - If missing token or missing required parameters
* @param {!String} permanentLink - The permanent link for the requested flow process.
* @param {?Number} [page=1] - The page number of the results given.
* @param {?Number} [perPage=0] - The number of content to return per page.
* @return {Promise<Flow>} - Promise object that returns success or failure
*
* @example
* import { flowService } from 'clinical6';
* flowService.getFlow('favorite_flow');
*/
getFlow(permanentLink, page = 1, perPage = 0) {
validate('FlowService.getFlow',
hasToken(),
isRequired({ permanentLink }),
isA({ permanentLink }, 'string'));
return new Promise((resolve, reject) => {
let httpQuery = `?page=${page}&per_page=${perPage}`;
httpQuery = ``;
Client.instance.fetch(`/api/v2/data_collection/flow_processes/${permanentLink}${httpQuery}`).then(
response => resolve(new Flow(Object.assign({}, { id: permanentLink }, response))),
err => reject(err)
)
.catch(err => reject(err));
});
}
/**
* Get Flow Data Group based on an id.
*
* @throws {Clinical6Error} - If missing token or missing required parameters
* @param {!Object} params - Parameters used to get information from server (such as id)
* @param {String} [cacheMode] - Override the caching method for this call
* @return {Promise<FlowDataGroup>} - Promise object that returns one object
*
* @example
* import { flowService, FlowDataGroup } from 'clinical6';
* flowService.getDataGroup({ id: 24 });
* const capturedValueGroup = new FlowDataGroup( {id: 24 });
* flowService.getDataGroup(capturedValueGroup);
*/
@deprecate
getDataGroup(params, cacheMode = undefined) {
validate('FlowService.getDataGroup',
hasToken(),
isRequired({ params }),
hasAttribute({ params }, 'id'));
validate('FlowService.getDataGroup',
hasToken(),
isRequired({ id: params.id }),
isA({ id: params.id }, 'number'));
const url = `/v3/data_collection/captured_value_groups/${params.id}`;
return super.get({ id: params.id, type: 'data_collection__captured_value_groups' }, { url, cacheMode });
}
/**
* Gets input data for a flow.
*
* Should really use the {@link Flow} methods instead that call this method.
*
* @throws {Clinical6Error} - If missing token or missing required parameters
* @param {!Flow} flow - Flow process from which data comes
* @return {Promise} - Promise object that returns success or failure
*
* @example
* import { flowService } from 'clinical6';
* flowService.getFlow('favorite_flow').then(flow => {
* flowService.getInputDataByFlow(flow);
* });
*/
getInputDataByFlow(flow) {
validate('FlowService.getInputData',
hasToken(),
isRequired({ flow }),
hasAttribute({ flow }, 'id'));
const id = flow.permanentLink;
const params = { };
if (flow.options.owner) {
params.owner = flow.options.owner;
}
if (flow.options.owner_type) {
params.owner_type = flow.options.owner_type;
}
if (flow.options.existing_id) {
params.existing_id = flow.options.existing_id;
}
if (flow.entry && flow.entry.captured_value_group && flow.entry.captured_value_group.id) {
params.captured_value_group_id = flow.entry.captured_value_group.id;
}
return new Promise((resolve, reject) => {
Client.instance.fetch(
`/api/v2/data_collection/flow_processes/${id}/captured_values/retrieve_all`,
'POST', params
).then(
(response) => {
response.captured_values.forEach(c => flow.set(c.input_id, c.value));
resolve(response.captured_values);
},
err => reject(err)
);
});
}
/**
* Gets the input data for a specific input id.
*
* Should really use the {@link Flow} methods instead that call this method.
*
* @throws {Clinical6Error} - If missing token or missing required parameters
* @param {!String} inputId - Flow Input ID of the input from which data comes
* @return {Promise} - Promise object that returns success or failure
*/
getInputDataById(inputId) {
validate('FlowService.getInputData',
hasToken(),
isRequired({ inputId }),
isA({ inputId }, 'string'));
Client.instance.fetch(`/api/collected_values/${inputId}`);
}
/**
* Call a GET request expecting JSON API information.
*
* @throws {Clinical6Error} - If missing token or missing required parameters
* @param {Object} [params] - Parameters used to get information from server (such as id)
* @param {String} [cacheMode] - Override the caching method for this call
* @return {Promise<FlowKeyValue[] | FlowKeyValue>}
* - Promise object that returns one keyValue or a keyValue array
*
* @example
* import { flowService } from 'clinical6';
*
* // You will be able to access these keyValues using the `getKeyValue` method.
* flowService.getKeyValue().then(keyValues => console.log(keyValues));
*
* // Additionally, you can retrieve a specific keyValue with the `getKeyValue`
* // method, using the ID of the desired keyValue as a parameter.
* flowService.getKeyValue({id: 23}).then(keyValue => console.log(keyValue));
*/
getKeyValue(params = {}, cacheMode = undefined) {
validate('FlowService.getKeyValue',
hasToken());
if (!params.type) {
params.type = 'data_collection__flow_process_values';
}
return super.get(params, { cacheMode });
}
/**
* @override
* Call a POST request on the main obj.type expecting JSON API information.
*
* @param {Object} [flow] - Parameters used to get information from server (such as id)
* @param {String} [cacheMode] - Override the caching method for this call
* @return {Promise<Flow>} - Inserted flow
*
* @example
* import { Flow, flowService } from 'clinical6';
* const flow = new Flow({...});
*
* // you can insert a flow using the `insert` method.
* flowService.insert(flow).then(flow => console.log(flow));
*
* // you could also just call `save` on the flow if it doesn't have an id, which will also
* // invoke the `insert` method
* flow.save();
*/
insert(flow, cacheMode = undefined) {
validate('FlowService.insert',
hasToken(),
isRequired({ flow }));
return super.insert(flow, { cacheMode });
}
/**
* Inserts the process values to start a flow data group / captured value group. This is how a flow starts and generates
* a captured value group id. If the group id already exists, this should also save it (though this is going to change)
*
* http://clinical6-docs.s3-website-us-east-1.amazonaws.com/apidoc/v3data_collectionflow_process_values/create.html
*
* @throws {Clinical6Error} - If missing token or missing required parameters
* @param {!FlowKeyValue} keyValue - The process value object used to save information
* @param {!Flow} keyValue.flow - The flow being started
* @param {!Number} keyValue.flow.id - The flow id, must be a number
* @param {?Entry} [keyValue.flow.entry] - The flow entry
* @param {!Number} keyValue.flow.entry.id - The flow entry id, must be a number
* @param {!Object} keyValue.owner - The owner of the data being collected
* @param {!String} keyValue.owner.type - The owner type, defaults to current logged in user type
* ('mobile_users' or 'users')
* @param {!String} keyValue.owner.id - The owner id, defaults to current logged in user's id
* @param {!Object} [keyValue.flowDataGroup] - The flow data group (captured value group)
* @param {!Number} keyValue.flowDataGroup.id - The flow data group id (captured value group id)
* @param {?String} [cacheMode] - A string to indicate how this is cached.
* @return {Promise<FlowKeyValue} - Promise object that returns the resulting flow process value
* (with flow data group id / captured value group id)
*
* @example
* import { Flow, FlowDataGroup, FlowKeyValue, flowService } from 'clinical6';
*
* const v = new FlowKeyValue(requestJsonApi);
* v.flow = new Flow({ id: 94 });
* v.flow.store();
* v.owner = new User({ id: 59 });
* v.owner.store();
* v.flowDataGroup = new FlowDataGroup({ id: 23 });
* v.flowDataGroup.store();
* v['56'] = 'male';
* v['57'] = 15;
* const response = await flowService.insertKeyValue(v);
*/
insertKeyValue(keyValue, cacheMode = undefined) {
const errorTitle = 'FlowService.insertKeyValue';
validate(errorTitle,
hasToken(),
isRequired({ keyValue }),
hasAttribute({ keyValue }, 'flow'),
isValid((keyValue instanceof FlowKeyValue), 'keyValue is not a type of FlowKeyValue'));
const { flow } = keyValue;
const owner = keyValue.owner || Client.instance.user;
validate(errorTitle,
isRequired({ flow, owner }),
hasAttribute({ flow, owner }, 'id'),
hasAttribute({ owner }, 'type'),
isA({ flowId: flow.id, ownerId: owner.id }, 'number'));
// If there is an entry, use the ediary flow process
const { entry } = flow;
let url = `/v3/data_collection/flow_process_values`;
if (entry) {
validate(errorTitle,
hasAttribute({ entry }, 'id'),
isA({ entryId: entry.id }, 'number'));
url = `/v3/ediary/entries/${entry.id}/data_collection/flow_process_values`;
}
return super.insert(keyValue, { url, cacheMode });
}
/**
* Transitions flow from one status to another.
*
* Should really use the {@link Flow} methods instead that call this method, such as
* {@link Flow#transition}, {@link Flow#start} or {@link Flow#complete}. Additionally, some of
* the {@link FlowStep} transition methods will trigger this as well.
*
* @throws {Clinical6Error} - If missing token or missing required parameters
* @param {!Flow} flow - Flow process to which collect from
* @param {!String} flow.id - The permanent link for the requested flow process.
* @param {!String} transition - the action to be applied to transition the flow
* @param {?Object} [options] - Various options to change the way the transition occurs
* @return {Promise} - Promise object that returns success or failure
*
* @example
* import { flowService } from 'clinical6';
* flowService.getFlow('favorite_flow').then(flow => {
* flowService.transition(flow, 'accept');
* });
*/
transition(flow, transition, options = {}) {
validate('FlowService.transition',
hasToken(),
isRequired({ flow, transition }),
hasAttribute({ flow }, 'id'),
isA({ transition }, 'string'));
const status = Object.assign({}, options, {
object: flow.permanentLink,
object_type: 'data_collection/flow_process',
transition,
});
return Client.instance.fetch(`/api/v2/data_collection/flow_processes/${flow.permanentLink}/collect`,
'post', { status });
}
/**
* @override
* Call a POST request on the main obj.type expecting JSON API information.
*
* @param {Object} [flow] - Parameters used to get information from server (such as id)
* @param {String} [cacheMode] - Override the caching method for this call
* @return {Promise<Flow>} - Inserted flow
*
* @example
* import { Flow, flowService } from 'clinical6';
* const flow = new Flow({...});
*
* // you can insert a flow using the `insert` method.
* flowService.insert(flow).then(flow => console.log(flow));
*
* // you could also just call `save` on the flow if it doesn't have an id, which will also
* // invoke the `insert` method
* flow.save();
*/
update(flow, cacheMode = undefined) {
validate('FlowService.update',
hasToken(),
isRequired({ flow }),
hasAttribute({ flow }, 'id'));
return super.update(flow, { cacheMode });
}
}
export default FlowService;