import { getOwner } from '@ember/application';
import { get } from '@ember/object';
import DS from 'ember-data';
import BaseBuilder from './base-builder';
import OrderByClause from './order-by-clause';
import QueryObject from './query-object';
import { createPredicate, SimplePredicate, ComplexPredicate, StringPredicate, DetailPredicate, DatePredicate, IsOfPredicate } from './predicate';
import Information from '../utils/information';
import isEmbedded from '../utils/is-embedded';
import { ConstParam, AttributeParam } from './parameter';
/**
* Class of builder for query.
* Uses method chaining.
*
* @module ember-flexberry-data
* @class Builder
* @extends BaseBuilder
*/
export default class Builder extends BaseBuilder {
/**
* @param store {Store} Store for building query.
* @param modelName {String} The name of the requested entity.
* @class Builder
* @constructor
*/
constructor(store, modelName) {
super();
if (!store || !(store instanceof DS.Store)) {
throw new Error('Store is not specified');
}
this._store = store;
this._localStore = getOwner(store).lookup('store:local');
this._modelName = modelName;
this._id = null;
this._projectionName = null;
this._predicate = null;
this._orderByClause = null;
this._idFromProjection = false;
this._isCount = false;
this._expand = {};
this._select = {};
this._dataType = null;
this._customQueryParams = {};
}
/**
* Sets the id of the requested entity.
*
* @method byId
* @param id {String|Number} The id of the requested entity.
* @return {Query.Builder} Returns this instance.
* @public
* @chainable
*/
byId(id) {
this._id = id;
return this;
}
/**
* Sets the name of the requested entity.
*
* @method from
* @param modelName {String} The name of the requested entity.
* @return {Query.Builder} Returns this instance.
* @public
* @chainable
*/
from(modelName) {
this._modelName = modelName;
return this;
}
/**
* Restricts the selectable objects to the specified type.
*
* @method isOf
* @param typeName {String} The model name of which the selectable objects should be assigned.
* @return {Query.Builder} Returns this instance.
* @public
* @chainable
*/
isOf(typeName) {
let model = this._store.modelFor(typeName);
if (!(model.prototype instanceof DS.Model)) {
throw new Error(`Unknown type: '${typeName}'.`);
}
let predicate = new IsOfPredicate(typeName);
this._predicate = this._predicate ? this._predicate.and(predicate) : predicate;
return this;
}
/**
*
* @method where
* @param ...args
* @return {Query.Builder} Returns this instance.
* @public
* @chainable
*/
where(...args) {
this._predicate = createPredicate(...args);
return this;
}
/**
*
* @method orderBy
* @param property {String}
* @return {Query.Builder} Returns this instance.
* @public
* @chainable
*/
orderBy(property) {
if (!property) {
throw new Error('You trying sort by a empty string.');
}
this._orderByClause = new OrderByClause(property);
return this;
}
/**
*
* @method top
* @param top {Number}
* @return {Query.Builder} Returns this instance.
* @public
* @chainable
*/
top(top) {
this._top = +top;
return this;
}
/**
*
* @method skip
* @param skip {Number}
* @return {Query.Builder} Returns this instance.
* @public
* @chainable
*/
skip(skip) {
this._skip = +skip;
return this;
}
/**
*
* @method count
* @return {Query.Builder} Returns this instance.
* @public
* @chainable
*/
count() {
this._isCount = true;
return this;
}
/**
* Adds attributes for selection.
* Automatically checks duplications.
*
* @method select
* @param attributes {String}
* @return {Query.Builder} Returns this instance.
* @public
* @chainable
*/
select(attributes) {
attributes.split(',').forEach(i => this._select[i.trim()] = true);
return this;
}
/**
* Adds attributes for selection from specified projection.
* Merges attributes with added using `select`.
*
* @method selectByProjection
* @param projectionName {String} The name of the projection.
* @param idFromProjection {Boolean}
* @return {Query.Builder} Returns this instance.
* @public
* @chainable
*/
selectByProjection(projectionName, idFromProjection) {
this._idFromProjection = idFromProjection;
this._projectionName = projectionName;
return this;
}
/**
*
* @method ofDataType
* @param dataType {String} The name of the data type.
* @return {Query.Builder} Returns this instance.
* @public
* @chainable
*/
ofDataType(dataType) {
this._dataType = dataType;
return this;
}
/**
*
* @method withCustomParams
* @param customQueryParams {Object}
* @return {Query.Builder} Returns this instance.
* @public
* @chainable
*/
withCustomParams(customQueryParams) {
this._customQueryParams = customQueryParams;
return this;
}
/**
* Builds query instance using all provided data.
*
* @method build
* @return {Object} Query instance.
* @public
*/
build() {
if (!this._modelName) {
throw new Error('Model name is not specified');
}
let tree;
let model = this._store.modelFor(this._modelName);
let isOfflineMode = this._store.offlineModels && this._store.offlineModels[this._modelName];
if (this._projectionName) {
let projection = model.projections.get(this._projectionName);
if (!projection) {
throw new Error(`Projection ${this._projectionName} for model ${this._modelName} is not specified`);
}
tree = this._getQueryTreeByProjection(projection, model, this._modelName, undefined, isOfflineMode);
} else {
tree = this._getQueryBySelect(this._select, model, this._modelName, isOfflineMode);
}
// Merge, don't replace.
let uniqSelect = {};
tree.select.forEach(i => uniqSelect[i] = true);
let select = Object.keys(uniqSelect);
let expand = tree.expand;
let primaryKeyName = tree.primaryKeyName;
let extendProperties = this._getExtendedProjection(model);
let extendTree = this._getQueryBySelect(extendProperties, model, this._modelName, isOfflineMode);
return new QueryObject(
this._modelName,
this._id,
this._projectionName,
this._predicate,
this._orderByClause,
this._top,
this._skip,
this._isCount,
expand,
select,
primaryKeyName,
extendTree,
this._customQueryParams,
this._dataType
);
}
_getQueryBySelect(select, model, modelName, isOfflineMode) {
let primaryKeyNameFromSerializer = isOfflineMode ? this._localStore.serializerFor(modelName).get('primaryKey') :
this._store.serializerFor(modelName).get('primaryKey');
let primaryKeyName = primaryKeyNameFromSerializer ? primaryKeyNameFromSerializer : 'id';
let result = {
select: ['id'],
expand: {},
primaryKeyName: primaryKeyName
};
let selectProperties = Object.keys(select);
let selectPropertieslength = selectProperties.length;
for (let i = 0; i < selectPropertieslength; i++) {
this._buildQueryForProperty(result, selectProperties[i], model, modelName, isOfflineMode);
}
return result;
}
_buildQueryForProperty(data, property, model, modelName, isOfflineMode) {
let pathItems = Information.parseAttributePath(property);
let relationshipsByName = get(model, 'relationshipsByName');
if (pathItems.length === 1) {
let attributeName = pathItems[0];
let modelAttributes = get(model, 'attributes');
if (attributeName === 'id' || modelAttributes.has(attributeName) || relationshipsByName.has(attributeName)) {
data.select.push(attributeName);
} else {
throw new Error(`Property '${attributeName}' in model '${modelName}' is not specified. ` +
`Please report this info to application support team or developers.`);
}
} else {
let key = pathItems.shift();
let relationship = relationshipsByName.get(key);
if (!relationship) {
throw new Error(`Property '${key}' in model '${modelName}' is not specified. Please report this info to application support team or developers.`);
}
let ralatedModelName = relationship.type;
let relatedModel = this._store.modelFor(ralatedModelName);
if (!data.expand[key]) {
let relationshipProps = {
async: relationship.options.async,
polymorphic: relationship.options.polymorphic,
isEmbedded: isEmbedded(this._store, model, key),
type: relationship.kind
};
let primaryKeyNameFromSerializer = isOfflineMode ? this._localStore.serializerFor(modelName).get('primaryKey') :
this._store.serializerFor(modelName).get('primaryKey');
let primaryKeyName = primaryKeyNameFromSerializer ? primaryKeyNameFromSerializer : 'id';
data.expand[key] = {
select: ['id'],
expand: {},
modelName: ralatedModelName,
primaryKeyName: primaryKeyName,
relationship: relationshipProps
};
}
this._buildQueryForProperty(data.expand[key], pathItems.join('.'), relatedModel, ralatedModelName, isOfflineMode);
}
}
_getQueryTreeByProjection(projection, model, modelName, relationshipProps, isOfflineMode) {
let primaryKeyNameFromSerializer = isOfflineMode ? this._localStore.serializerFor(modelName).get('primaryKey') :
this._store.serializerFor(modelName).get('primaryKey');
let primaryKeyName = primaryKeyNameFromSerializer ? primaryKeyNameFromSerializer : 'id';
let tree = {
select: this._idFromProjection ? [] : ['id'],
expand: {},
modelName: modelName,
primaryKeyName: primaryKeyName,
relationship: relationshipProps
};
let attributes = projection.attributes;
for (let attrName in attributes) {
if (attributes.hasOwnProperty(attrName)) {
let attr = attributes[attrName];
switch (attr.kind) {
case 'attr':
tree.select.push(attrName);
break;
case 'hasMany':
case 'belongsTo': {
let relationshipsByName = get(model, 'relationshipsByName');
let relationship = relationshipsByName.get(attrName);
let ralatedModelName = relationship.type;
let relatedModel = this._store.modelFor(ralatedModelName);
let relationshipProps = {
async: relationship.options.async,
polymorphic: relationship.options.polymorphic,
isEmbedded: isEmbedded(this._store, model, attrName),
type: attr.kind
};
tree.select.push(attrName);
tree.expand[attrName] = this._getQueryTreeByProjection(attr, relatedModel, ralatedModelName, relationshipProps, isOfflineMode);
break;
}
default:
throw new Error(`Unknown kind of projection attribute: ${attr.kind}`);
}
}
}
return tree;
}
_getExtendedProjection(model) {
let _this = this;
let extend = [];
let existKeys = Object.keys(this._select);
let scanPredicates = function(predicate, detailPath) {
if (predicate instanceof SimplePredicate || predicate instanceof StringPredicate || predicate instanceof DatePredicate) {
let attributesPaths = [];
let pathOfFirstArgument = predicate.attributePath;
if (!(pathOfFirstArgument instanceof ConstParam)) {
attributesPaths.push(pathOfFirstArgument instanceof AttributeParam
? pathOfFirstArgument.attributePath
: pathOfFirstArgument);
}
if (predicate.value instanceof AttributeParam) {
attributesPaths.push(predicate.value.attributePath);
}
attributesPaths.forEach((attributesPath) => {
let path = detailPath ? detailPath + '.' : '';
Information.parseAttributePath(attributesPath).forEach((attribute) => {
let key = `${path}${attribute}`;
if (existKeys.indexOf(key) === -1) {
extend[key.trim()] = true;
}
path += `${attribute}.`;
});
});
}
if (predicate instanceof DetailPredicate) {
scanPredicates(predicate.predicate, predicate.detailPath);
}
if (predicate instanceof ComplexPredicate) {
predicate.predicates.forEach((innerPredicate) => {
scanPredicates(innerPredicate, detailPath);
});
}
};
scanPredicates(this._predicate);
if (this._orderByClause) {
for (let i = 0; i < this._orderByClause.length; i++) {
let path = '';
let attributePath = Information.parseAttributePath(this._orderByClause.attribute(i).name);
for (let i = 0; i < attributePath.length; i++) {
let key = `${path}${attributePath[i]}`;
if (existKeys.indexOf(key) === -1) {
extend[key.trim()] = true;
}
path += `${attributePath[i]}.`;
}
}
}
// Add all related objects if no projection or select.
if (!this._projectionName && Object.keys(this._select).length === 0) {
let maxDeepLevel = 5; // TODO: May be configure it?
let greedyProjectionMaker = function(model, deepLevel, path) {
if (deepLevel > maxDeepLevel) {
return;
}
let modelAttributes = get(model, 'attributes');
modelAttributes.forEach(function(meta, name) {
extend[`${path}${name}`] = true;
});
let relationshipsByName = get(model, 'relationshipsByName');
relationshipsByName.forEach(function(meta, name) {
extend[`${path}${name}`] = true;
let ralatedModelName = meta.type;
let relatedModel = _this._store.modelFor(ralatedModelName);
greedyProjectionMaker(relatedModel, deepLevel + 1, `${path}${name}.`);
});
};
greedyProjectionMaker(model, 0, '');
}
return extend;
}
}