busy-data
Version:
The default blueprint for ember-cli addons.
380 lines (330 loc) • 11.5 kB
JavaScript
/**
* @module store
*
*/
import Mixin from '@ember/object/mixin';
import { isArray } from '@ember/array';
import { isNone, isEmpty } from '@ember/utils';
import { get, set, getWithDefault } from '@ember/object';
import { merge } from '@ember/polyfills';
import { run } from '@ember/runloop';
import { Promise as EmberPromise, resolve, all } from 'rsvp';
import { runInDebug, assert } from '@ember/debug';
import DS from 'ember-data';
/***/
const { PromiseArray } = DS;
/**
* `StoreFinders`
*
* Mixin that adds extra find methods to the store service
*/
export default Mixin.create({
_maxPageSize: 80,
findAll(modelType, _query={}) {
// copy query object
const query = merge({}, _query);
if (isNone(get(query, 'limit'))) {
set(query, 'page_size', getWithDefault(query, 'page_size', this._maxPageSize));
set(query, 'page', getWithDefault(query, 'page', 1));
}
// make query request
return this.query(modelType, query).then(models => {
let nextQuery = {};
if (nextParams(models, nextQuery, query)) {
return this.findAll(modelType, nextQuery).then(moreModels => {
if (isObject(moreModels) && !isEmpty(get(moreModels, 'content'))) {
models.pushObjects(get(moreModels, 'content'));
}
return models;
});
} else {
return models;
}
});
},
findByIds(modelType, ids, query={}) {
query.deleted_on = '_-DISABLE-_';
return this.findWhereIn(modelType, 'id', ids, query);
},
findWhereIn(modelType, key, values, query={}) {
assert('modelType must be of type string in store.findWhereIn()', typeof modelType === 'string');
assert('key must be of type string in store.findWhereIn()', typeof key === 'string');
assert('values must be an array of strings in store.findWhereIn()', isArray(values));
assert('query must be an object in store.findWhereIn()', typeof query === 'object');
const _values = values.slice(0);
const queryList = [];
while(_values.length) {
const __values = _values.splice(0, 10);
if (/^!/.test(key)) {
queryList.push(
merge({ _not_in: { [key]: __values } }, query)
);
} else {
queryList.push(
merge({ _in: { [key]: __values } }, query)
);
}
}
return this._findRecords(modelType, queryList, null);
},
/**
* Simple rpc request method that does not use the ember-data
* model layer.
*
* @public
* @method rpcRequest
* @param type {string} The RPC client to call the method
* @param method {string} The RPC method on the client
* @param params {object} The params to send to the method
* @param baseURL {string} Optional, Override url to the rpc client if different from the normal baseURL.
* @return {EmberPromise}
*/
rpcRequest(client, method, params={}, baseURL='') {
assert('client must be of type string in store.rpcRequest()', typeof client === 'string');
assert('method must be of type string in store.rpcRequest()', typeof method === 'string');
assert('params must be an object in store.rpcRequest()', !isNone(params) && typeof params === 'object');
assert('baseURL must be of type string in store.rpcRequest()', typeof baseURL === 'string');
const adapter = this._instanceCache.get('adapter');
assert("In order to use rpcRequest your must include the rpc-adapter mixin in your adapter", !isNone(adapter.rpcRequest));
// call the rpc method and return the promise.
return adapter.rpcRequest(this, client, method, params, baseURL);
},
/**
*
* TODO: test this
* add recordArray.update method for update calls to _findRecords
*
*/
_findRecords(modelName, query, array) {
array = array || this.recordArrayManager.createAdapterPopulatedRecordArray(modelName, query);
set(array, '_update', () => {
return this._findRecords(get(array, 'modelName'), get(array, 'query'), array);
});
let adapter = this.adapterFor(modelName);
assert(`You tried to load a query but you have no adapter (for ${modelName})`, adapter);
assert(`You tried to load a query but your adapter does not implement 'query'`, typeof adapter.query === 'function');
// adapter.query needs the class
let modelClass = this.modelFor(modelName);
// call findRecords and return the promsie
let promise;
if (isArray(query)) {
promise = _findRecordQueue(adapter, this, modelClass, query.slice(0)).then(payload => _processResults(this, array, payload));
} else {
promise = _findRecords(adapter, this, modelClass, query).then(payload => _processResults(this, array, payload));
}
return PromiseArray.create({ promise });
}
});
function _findRecordQueue(adapter, store, modelClass, queryObjects) {
// no queryObjects then return a default jsonapi response
if (queryObjects.length <= 0) {
return resolve({
data: [],
included: [],
jsonapi: { version: '1.0' },
meta: { returnedRows: 0 }
});
}
return new EmberPromise((resolve, reject) => {
const promises = [];
// call _findRecords on each query object
queryObjects.forEach(query => promises.push(_findRecords(adapter, store, modelClass, query)));
// parse the results of all calls an merge them into one result
all(promises).then(results => _mergeRecords(results))
.then(records => get(records, 'errors') ? run(null, reject, records) : run(null, resolve, records))
.catch(err => run(null, reject, err));
});
}
/**
* generic api getter for making callse to the api.
*
* NOTE:
* This call returns a jsonapi formatted object. _processResults
* must be called to convert the object to a recordArray.
*
* @method _findRecords
* @params adapter {DS.Adapter}
* @params store {DS.Store}
* @params modelClass {DS.Model}
* @params query {Object}
* @returns {Object}
*/
function _findRecords(adapter, store, modelClass, query) {
let promise = adapter.query(store, modelClass, query);
let label = `DS: Handle Adapter#query of ${modelClass}`;
promise = EmberPromise.resolve(promise, label);
promise = _guard(promise, _bind(_objectIsAlive, store));
return promise.then(adapterPayload => {
let serializer = serializerForAdapter(store, adapter, modelClass.modelName);
let payload = normalizeResponseHelper(serializer, store, modelClass, adapterPayload, null, 'query');
return payload;
}, null, `DS: Extract payload of query ${modelClass.modelName}`);
}
function _processResults(store, recordArray, payload) {
let internalModels = store._push(payload);
assert('The response to store.query is expected to be an array but it was a single record. Please wrap your response in an array or use `store.queryRecord` to query for a single record.', Array.isArray(internalModels));
recordArray._setInternalModels(internalModels, payload);
return recordArray;
}
function _mergeRecords(records) {
let recordOut
records.forEach(record => {
if (!recordOut) {
recordOut = record;
} else {
// get total rows
let rows = getWithDefault(recordOut, 'meta.returnedRows', 0) + getWithDefault(record, 'returnedRows', 0);
set(recordOut, 'meta.returnedRows', rows);
// merged data arrays
set(recordOut, 'data', get(recordOut, 'data').concat(get(record, 'data')));
// combine api errors
if (get(recordOut, 'errors') && get(record, 'errors')) {
set(recordOut, 'errors', get(recordOut, 'errors').concat(get(record, 'errors')));
} else if (!get(recordOut, 'errors') && get(record, 'errors')) {
set(recordOut, 'errors', get(record, 'errors'));
}
}
});
return recordOut;
}
function _bind(fn, ...args) {
return function() {
return fn.apply(undefined, args);
};
}
function _guard(promise, test) {
let guarded = promise['finally'](function() {
if (!test()) {
guarded._subscribers.length = 0;
}
});
return guarded;
}
function _objectIsAlive(object) {
return !(get(object, "isDestroyed") || get(object, "isDestroying"));
}
function serializerForAdapter(store, adapter, modelName) {
let serializer = adapter.serializer;
if (serializer === undefined) {
serializer = store.serializerFor(modelName);
}
if (serializer === null || serializer === undefined) {
serializer = {
extract(store, type, payload) { return payload; }
};
}
return serializer;
}
function validateDocumentStructure(doc) {
let errors = [];
if (!doc || typeof doc !== 'object') {
errors.push('Top level of a JSON API document must be an object');
} else {
if (!('data' in doc) &&
!('errors' in doc) &&
!('meta' in doc)) {
errors.push('One or more of the following keys must be present: "data", "errors", "meta".');
} else {
if (('data' in doc) && ('errors' in doc)) {
errors.push('Top level keys "errors" and "data" cannot both be present in a JSON API document');
}
}
if ('data' in doc) {
if (!(doc.data === null || Array.isArray(doc.data) || typeof doc.data === 'object')) {
errors.push('data must be null, an object, or an array');
}
}
if ('meta' in doc) {
if (typeof doc.meta !== 'object') {
errors.push('meta must be an object');
}
}
if ('errors' in doc) {
if (!Array.isArray(doc.errors)) {
errors.push('errors must be an array');
}
}
if ('links' in doc) {
if (typeof doc.links !== 'object') {
errors.push('links must be an object');
}
}
if ('jsonapi' in doc) {
if (typeof doc.jsonapi !== 'object') {
errors.push('jsonapi must be an object');
}
}
if ('included' in doc) {
if (typeof doc.included !== 'object') {
errors.push('included must be an array');
}
}
}
return errors;
}
function normalizeResponseHelper(serializer, store, modelClass, payload, id, requestType) {
let normalizedResponse = serializer.normalizeResponse(store, modelClass, payload, id, requestType);
let validationErrors = [];
runInDebug(() => {
validationErrors = validateDocumentStructure(normalizedResponse);
});
assert(`normalizeResponse must return a valid JSON API document:\n\t* ${validationErrors.join('\n\t* ')}`, isEmpty(validationErrors));
return normalizedResponse;
}
function isObject(value) {
return !isNone(value) && typeof value === 'object' && value.length === undefined;
}
function nextParams(model, query, lastQuery) {
let isJsonApi = false;
let next = get(model, 'meta.next');
if (isNone(next)) {
isJsonApi = true;
next = get(model, 'links.next');
}
if (!isEmpty(next)) {
if (isJsonApi) {
let [ , params ] = next.split('?');
params = params.split('&');
params.forEach(item => {
let [ key, value ] = item.split('=');
if (/\[/.test(key)) {
let [ key2, key3 ] = key.split('[');
key3 = key3.replace(/\]$/, '');
key = key2;
if (/\d+/.test(key3)) {
value = [value];
} else {
value = {[key3]: value};
}
}
set(query, key, value);
});
} else {
lastQuery.page = lastQuery.page + 1;
merge(query, lastQuery);
}
return true;
}
return false;
}
//function _findWhereIn(store, modelType, queryList, query) {
// if (queryList.length === 0) {
// return resolve([]);
// }
//
// // get next query params
// const params = queryList.shift();
//
// // merge query params with params
// merge(params, query);
//
// // find all models
// return store.findAll(modelType, params).then((model) => {
// return _findWhereIn(store, modelType, queryList, query).then((moreModels) => {
// if (isObject(moreModels) && !isEmpty(get(moreModels, 'content'))) {
// return model.pushObjects(get(moreModels, 'content'));
// }
// return model;
// });
// });
//}