src/services/JsonApiService.js
import Client from '../Client';
import AbstractService from './AbstractService';
import { objectToURI, stringToSnake } from '../utilities/FormatUtility';
import {
hasAttribute,
hasToken,
isA,
isRequired,
isValid,
validate,
} from '../utilities/ValidationUtility';
import { helperFactory } from '../utilities/factories/HelperFactory';
/**
* An abstract class to handle generic get, insert, update, delete json api calls.
*
* Note: This class is intended to be extended and not used directly.
*/
class JsonApiService extends AbstractService {
constructor(type, options = { title: 'JsonApiService', obj: 'obj', tokenRequired: [] }) {
super(type);
this.options = options;
}
/**
* Call a DELETE request on the main obj.type expecting JSON API information.
*
* @param {Object} [obj] - Object to be deleted
* @param {String} [options] - Modify the nature of the call and response
* @param {String} [options.url] - Override the url for this call
* @param {String} [options.cacheMode] - Override the caching method for this call
* @return {Promise} - Message
*/
async delete(obj, options = {}) {
// Validation
const action = `delete`;
const title = `${this.options.title}.${action}`;
if (this.options.tokenRequired.indexOf(action) >= 0) {
validate(title, hasToken());
}
const objMap = {};
objMap[`${this.options.obj || 'obj'}`] = obj;
validate(title,
isRequired(objMap),
hasAttribute(objMap, 'id'),
hasAttribute(objMap, 'type'));
// set defaults
const mode = options.cacheMode || this.cacheMode;
const type = obj.type.replace('__', '/');
let url = options.url || `/v3/${type}/${obj.id}`;
['url', 'cacheMode'].forEach(v => delete options[v]);
// set url options
if (Object.keys(options).length > 0) {
url = `${url}?${objectToURI(options, 'options')}`;
}
return Client.instance.request(obj.type, Client.instance.fetch(url, 'delete'), { mode, id: obj.id, clear: true });
}
/**
* Call a GET request expecting JSON API information.
*
* @param {Object} [params] - Parameters used to get information from server
* @param {Number} [params.id] - Id to get data from the server
* @param {String} [params.type] - Type to be used for storage
* @param {Object} [params.filters] - Filters to be used for get
* @param {Object} [params.page] - Pagination object
* @param {Object} [params.page.size] - Size of each page
* @param {Object} [params.page.number] - The current page
* @param {Object} [params.page.first] - The first page of data
* @param {Object} [params.page.last] - The last page of data
* @param {Object} [params.page.prev] - The previous page of data
* @param {Object} [params.page.next] - The next page of data
* @param {String} [params.sort] - The string on how things should be sorted, such as sort1,-sort2,sort3
* @param {String} [options] - Modify the nature of the call and response
* @param {String} [options.url] - Override the url for this call
* @param {String} [options.cacheMode] - Override the caching method for this call
* @param {String} [options.meta] - Pass and retrieve through reference meta data
* @return {Promise} - Promise with data (array or object)
*/
async get(params = {}, options = {}) {
// Validation
const action = `get`;
const title = `${this.options.title}.${action}`;
if (this.options.tokenRequired.indexOf(action) >= 0) {
validate(title, hasToken());
}
// set defaults
let key = 'id';
if (options.key && params[options.key]) {
({ key } = options);
} else if (this.options.key && params[this.options.key]) {
({ key } = this.options);
}
const id = (params[key] !== undefined) ? `/${params[key]}` : '';
const type = (params.type !== undefined) ? params.type : this.type;
let url = options.url || `/v3/${type.replace('__', '/')}${id}`;
// update caching to return an array if there is no id
if (params[key] === undefined) {
options = Object.assign({ asArray: true }, options);
}
// check filters
if (params.filters) {
validate(title, isA({ filters: params.filters }, Object));
}
// check page
if (params.page) {
validate(title, isA(params.page, 'number'));
}
// check sort
if (params.sort) {
validate(title,
isA({ sort: stringToSnake(params.sort) }, 'string'),
isValid(/^(((-?\w)(\..*)?){1},?)+[^,.]$/.test(params.sort), 'sort format must be like "sort1,-sort2,sort3,sort4.property"'));
}
// check search
if (params.search) {
validate(title, isA({ search: params.search }, 'string'));
}
const urlParams = objectToURI(
['filters', 'page', 'sort', 'search']
.filter(f => params[f])
.reduce((o, k) => { o[k] = params[k]; return o; }, {})
);
if (urlParams !== '') {
url = `${url}?${urlParams}`;
}
// Call request, store, return to calling command
return Client.instance.request(type,
Client.instance.fetch(url).then(response => helperFactory.get(response, undefined, options.meta)),
{
mode: options.cacheMode || this.cacheMode,
asArray: options.asArray,
id: params[key]
});
}
/**
* Call a GET request expecting JSON API information for children given a parent.
*
* @param {Object} parent - Parameters used to get information from server
* @param {Number} parent.id - Id to get data from the server
* @param {String} parent.type - Type to be used for storage
* @param {Object} [child] - Parameters used to get information from server
* @param {Number} [child.id] - Id to get data from the server
* @param {String} [child.type] - Type to be used for storage
* @param {Object} [child.filters] - Filters to be used for get
* @param {Object} [child.page] - Pagination object
* @param {Number} [child.page.size] - Size of each page
* @param {Number} [child.page.number] - The current page
* @param {Number} [child.page.first] - The first page of data
* @param {Number} [child.page.last] - The last page of data
* @param {Number} [child.page.prev] - The previous page of data
* @param {Number} [child.page.next] - The next page of data
* @param {String} [options] - Modify the nature of the call and response
* @param {String} [options.url] - Override the url for this call
* @param {String} [options.cacheMode] - Override the caching method for this call
* @param {String} [options.meta] - Pass and retrieve through reference meta data
* @return {Promise} - Promise with data (array or object)
*
* @example
* import { siteService, mobileUserService, SiteMember } from 'clinical6';
*
* siteService.getChildren(site, { type: 'trials/site_members' });
* siteService.getChildren(site, new SiteMember());
* mobileUserService.getChildren(user, { type: 'ediary/entries' }); // generates /v3/mobile_users/:id/ediary/entries
*/
async getChildren(parent, child = {}, options = {}) {
// Validation
const action = `getChildren`;
const title = `${this.options.title}.${action}`;
if (this.options.tokenRequired.indexOf(action) >= 0) {
validate(title, hasToken());
}
validate(title,
hasAttribute({ parent }, 'id'),
hasAttribute({ child }, 'type'));
// set defaults
let key = 'id';
if (options.key && parent[options.key]) {
({ key } = options);
} else if (this.options.key && parent[this.options.key]) {
({ key } = this.options);
}
const parentId = (parent[key] !== undefined) ? `/${parent[key]}` : '';
const childId = (child[key] !== undefined) ? `/${child[key]}` : '';
const parentType = (parent.type !== undefined) ? parent.type : this.type;
let url = options.url || `/v3/${parentType.replace('__', '/')}${parentId}`;
url += `/${child.type.replace('__', '/')}${childId}`;
// update caching to return an array if there is no id
if (parent[key] === undefined) {
options = Object.assign({ asArray: true }, options);
}
// check filters
if (child.filters) {
validate(title, isA({ filters: child.filters }, Object));
}
// check page
if (child.page) {
validate(title, isA(child.page, 'number'));
}
// check sort
if (child.sort) {
validate(title,
isA({ sort: child.sort }, 'string'),
isValid(/^(((-?\w)(\..*)?){1},?)+[^,.]$/.test(child.sort), 'sort format must be like "sort1,-sort2,sort3"'));
}
const urlParams = objectToURI(
['filters', 'page', 'sort']
.filter(f => child[f])
.reduce((o, k) => { o[k] = child[k]; return o; }, {})
);
if (urlParams !== '') {
url = `${url}?${urlParams}`;
}
// Call request, store, return to calling command
return Client.instance.request(child.type,
Client.instance.fetch(url).then(response => helperFactory.get(response, undefined, options.meta)),
{
mode: options.cacheMode || this.cacheMode,
asArray: options.asArray,
id: child[key]
});
}
/**
* Call a POST request on the main obj.type expecting JSON API information.
*
* @param {Object} [obj] - Object to be inserted
* @param {String} [options] - Modify the nature of the call and response
* @param {String} [options.url] - Override the url for this call
* @param {String} [options.cacheMode] - Override the caching method for this call
* @param {Object} [options.meta] - Pass and retrieve through reference meta data
* @return {Promise} - Promise with object
*/
async insert(obj, options = {}) {
// Validation
const action = `insert`;
const title = `${this.options.title}.${action}`;
if (this.options.tokenRequired.indexOf(action) >= 0) {
validate(title, hasToken());
}
const objMap = {};
objMap[`${this.options.obj || 'obj'}`] = obj;
validate(title,
isRequired(objMap),
hasAttribute(objMap, 'type'));
// set defaults
const type = (obj.type || this.type).replace('__', '/');
const url = options.url || `/v3/${type}`;
const headers = options.headers || {};
const params = { data: obj };
if (options.meta && Object.keys(options.meta).length > 0) {
params.meta = options.meta;
} else if (!options.meta) {
options.meta = {};
}
return Client.instance.request(obj.type,
Client.instance.fetch(url, 'post', params, headers).then((response) => {
if (response && (response.id || (response.data && response.data.id))) {
obj.id = parseInt(response.id || response.data.id, 10);
}
return helperFactory.get(response, undefined, options.meta);
}),
{ mode: options.cacheMode || this.cacheMode });
}
/**
* Call a PATCH request on the main obj.type expecting JSON API information.
*
* @param {Object} [obj] - Object to be updated
* @param {String} [options] - Modify the nature of the call and response
* @param {String} [options.url] - Override the url for this call
* @param {String} [options.cacheMode] - Override the caching method for this call
* @return {Promise} - Promise with object
*/
async update(obj, options = {}) {
// Validation
const action = `update`;
const title = `${this.options.title}.${action}`;
if (this.options.tokenRequired.indexOf(action) >= 0) {
validate(title, hasToken());
}
const objMap = {};
objMap[`${this.options.obj || 'obj'}`] = obj;
validate(title,
isRequired(objMap),
hasAttribute(objMap, 'type'));
// set defaults
const type = obj.type.replace('__', '/');
const url = options.url || `/v3/${type}/${obj.id}`;
return Client.instance.request(obj.type,
Client.instance.fetch(url, 'patch', { data: obj, meta: options.meta }).then(response => helperFactory.get(response)),
{ mode: options.cacheMode || this.cacheMode, id: obj.id });
}
}
export default JsonApiService;