@nsilly/repository
Version:
NF Repository is a abstract layer of Sequelize Application, that make application more easy to understand and flexible to maintain.
899 lines (848 loc) • 23.5 kB
JavaScript
import _ from 'lodash';
import { Exception, NotFoundException } from '@nsilly/exceptions';
import { QueryBuilder } from './Utils/QueryBuilder';
import { LengthAwarePaginator } from '@nsilly/response';
import { Request } from '@nsilly/support';
export class Repository {
constructor() {
this.builder = new QueryBuilder();
this.paranoid = true;
this.raw = null;
}
/**
* Set Raw option
*
* @param {Boolean} raw Allow get raw data from database
*
* @return this
*/
setRaw(raw) {
if (!_.isBoolean(raw)) {
throw new Exception('Raw option can be boolean only');
}
this.raw = raw;
return this;
}
/**
* Create or update a record matching the attributes, and fill it with values
*
* @param {Object} attributes params to find resource
* @param {Object} values params to update or create new resource
*
* @return Object
*/
async updateOrCreate(attributes, values) {
if (_.isNil(attributes)) {
throw new Exception('attributes should not empty', 1000);
}
const item = await this.Models().findOne({
where: attributes
});
let result;
if (item) {
result = await item.update(values);
} else {
result = await this.Models().create(values);
}
return result;
}
/**
* Save a new model and return the instance.
*
* @param {Object} attributes create new resource with given params
*
* @return Object
*/
async create(attributes) {
if (_.isNil(attributes)) {
throw new Exception('attributes should not empty', 1000);
}
const result = await this.Models().sequelize.transaction(
function(t) {
return this.Models().create(attributes, { transaction: t });
}.bind(this)
);
// add many to many associations before saving if there is
const associations = this.Models().associations;
const manyToManyAssociations = Object.keys(associations).filter(association => {
return associations[association].associationType === 'BelongsToMany';
});
for (let n = 0; n < Object.keys(attributes).length; n++) {
const attributeKey = Object.keys(attributes)[n];
const attributeVal = attributes[attributeKey];
if (manyToManyAssociations.indexOf(attributeKey) > -1) {
if (attributeVal) {
for (let i = 0; i < attributeVal.length; i++) {
await result['add' + _.capitalize(attributeKey)](attributeVal[i]);
}
}
}
}
if (_.isNil(result)) {
throw new Exception('Can not create resource', 1004);
}
return result;
}
/**
* Find the first resource that match with given params or create new one if not exist
*
* @param {Object} attributes find or create new resource with given params
*
* @return Object
*/
async firstOrCreate(attributes) {
let result;
if (_.isNil(attributes)) {
throw new Exception('attributes should not empty', 1000);
}
result = await this.Models().findOne({
where: attributes
});
if (!result) {
result = await this.Models().sequelize.transaction(
function(t) {
return this.Models().create(attributes, { transaction: t });
}.bind(this)
);
const associations = this.Models().associations;
const manyToManyAssociations = Object.keys(associations).filter(association => {
return associations[association].associationType === 'BelongsToMany';
});
for (let n = 0; n < Object.keys(attributes).length; n++) {
const attributeKey = Object.keys(attributes)[n];
const attributeVal = attributes[attributeKey];
if (manyToManyAssociations.indexOf(attributeKey) > -1) {
if (attributeVal) {
for (let i = 0; i < attributeVal.length; i++) {
await result['add' + _.capitalize(attributeKey)](attributeVal[i]);
}
}
}
}
if (_.isNil(result)) {
throw new Exception('Can not create resource', 1004);
}
}
return result;
}
/**
* Update multiple instances that match the where options
*
* @param {Object} attributes values to update resource
* @param {Integer} id Update resource with given ID
*
* @return Object
*/
async update(attributes, id = undefined) {
let result;
if (_.isNil(attributes)) {
throw new Exception('attributes should not empty', 1000);
}
if (_.isUndefined(id)) {
result = await this.Models().update(attributes, {
where: this.getWheres()
});
} else {
const item = await this.findById(id);
result = await item.update(attributes);
}
return result;
}
async bulkCreate(attributesArr, individual = false) {
const result = await this.Models().bulkCreate(attributesArr, {
individualHooks: individual
});
return result;
}
bulkUpsert(attributes) {
if (Array.isArray(attributes)) {
return Promise.all(
attributes.map(attribute => {
return this.singleUpsert(attribute);
})
);
} else {
return this.singleUpsert(attributes);
}
}
async replaceRelations(result, attributes, extraInfo = {}) {
const associations = Object.keys(this.Models().associations);
for (let n = 0; n < Object.keys(attributes).length; n++) {
const attributeKey = Object.keys(attributes)[n];
const attributeVal = attributes[attributeKey];
if (associations.indexOf(attributeKey) > -1) {
if (attributeVal) {
const data = await result['get' + _.capitalize(attributeKey)]();
// to make sure only many to any relationship records will be deleted before adding the new one
// when attribute value is not an array we dont want to do any deletion since it is going to be one to one relation
// for one to one it is safe to replace straight away as it just an update inside the current table
if (Array.isArray(data) && Array.isArray(attributeVal)) {
for (let i = 0; i < data.length; i++) {
// if value given is already related then dont do anything
if (attributeVal.indexOf(data[i].id) === -1) {
await data[i].destroy();
}
}
}
if (typeof extraInfo[attributeKey] !== 'undefined') {
// if extra info is an array then we delete all the relations and re add one by one
// since sequelize through doesnt support array for "through" options
if (Array.isArray(extraInfo[attributeKey])) {
await result['set' + _.capitalize(attributeKey)](null);
for (let x = 0; x < extraInfo[attributeKey].length; x++) {
await result['add' + _.capitalize(attributeKey)]([attributeVal[x]], {
through: extraInfo[attributeKey][x]
});
}
} else {
await result['set' + _.capitalize(attributeKey)](attributeVal, {
through: extraInfo[attributeKey]
});
}
} else {
await result['set' + _.capitalize(attributeKey)](attributeVal);
}
}
}
}
return result;
}
getNonRelationAttribute(attributes) {
const associations = Object.keys(this.Models().associations);
const newAttribute = {};
for (let n = 0; n < Object.keys(attributes).length; n++) {
const attributeKey = Object.keys(attributes)[n];
const attributeVal = attributes[attributeKey];
if (associations.indexOf(attributeKey) === -1) {
newAttribute[attributeKey] = attributeVal;
}
}
return newAttribute;
}
async singleUpsert(attribute) {
const identifier = {};
const attributeParsed = {};
const additionalRelationInfo = {};
let result;
Object.keys(attribute).forEach(key => {
if (key.indexOf('__relation_info__') === -1) {
if (key.indexOf('__unique__') > -1) {
if (attribute[key]) {
identifier[key.replace('__unique__', '')] = attribute[key];
}
} else {
attributeParsed[key] = attribute[key];
}
} else {
additionalRelationInfo[key.replace('__relation_info__', '')] = attribute[key];
}
});
if (Object.keys(identifier).length > 0) {
const item = await this.Models().findOne({
where: identifier
});
if (item) {
// just update the non relation fields
result = await item.update(this.getNonRelationAttribute(attributeParsed));
await this.replaceRelations(result, attributeParsed, additionalRelationInfo);
} else {
result = await this.Models().create({
...this.getNonRelationAttribute(attributeParsed),
...identifier
});
await this.replaceRelations(result, attributeParsed, additionalRelationInfo);
}
} else {
result = await this.Models().create(this.getNonRelationAttribute(attributeParsed));
await this.replaceRelations(result, attributeParsed, additionalRelationInfo);
}
return result;
}
bulkDelete(attributes) {
if (Array.isArray(attributes)) {
return Promise.all(
attributes.map(attribute => {
return this.Models().destroy({
where: attribute
});
})
);
} else {
return this.Models().destroy({
where: attributes
});
}
}
/**
* Get the first record
*
* @return Object
*/
async first() {
let params = {
where: this.getWheres(),
include: this.getIncludes(),
order: this.getOrders()
};
if (_.isBoolean(this.raw)) {
params = { ...params, ...{ raw: this.raw } };
}
if (_.isArray(this.getAttributes()) && this.getAttributes().length > 0) {
params = _.assign(params, { attributes: this.getAttributes() });
}
let model = this.Models();
if (this.getScopes().length > 0) {
model = model.scope(this.getScopes());
}
const result = await model.findOne(params);
return result;
}
/**
* Execute the query and get the first result or throw an exception
*
* @return Object
*/
async firstOrFail() {
const result = this.first();
if (!result) {
throw new NotFoundException('Resource');
}
return result;
}
/**
* Find a model by its primary key.
*
* @param {Integer} ID find resource with given ID
*
* @return Boolean
* @throws Exception
*/
async findById(id) {
let params = {
where: {
id: id
},
include: this.getIncludes(),
paranoid: this.paranoid
};
if (_.isArray(this.getAttributes()) && this.getAttributes().length > 0) {
params = _.assign(params, { attributes: this.getAttributes() });
}
let model = this.Models();
if (this.getScopes().length > 0) {
model = model.scope(this.getScopes());
}
const result = await model.findOne(params);
if (!result) {
throw new NotFoundException('Resource');
}
return result;
}
/**
* Delete a model by its primary key.
*
* @param {Integer} id delete resource with given ID
*
* @return Boolean
* @throws Exception
*/
async deleteById(id, options = {}) {
const item = await this.findById(id);
let result;
if (!_.isUndefined(options.force) && options.force === true) {
result = await item.destroy({ force: true });
}
result = await item.destroy();
if (result === false) {
throw new Exception('can not delete resource', 1002);
}
return result;
}
/**
* Delete resources by given condition
*
* @return Boolean
* @throws Exception
*/
async delete(options = {}) {
let result;
if (!_.isUndefined(options.force) && options.force === true) {
result = await this.Models().destroy({
where: this.getWheres(),
force: true
});
} else {
result = await this.Models().destroy({
where: this.getWheres()
});
}
return result;
}
/**
* Execute the query as a "select" statement.
*
* @return Array
*/
async get() {
let params = {
where: this.getWheres(),
include: this.getIncludes(),
order: this.getOrders(),
group: this.getGroup(),
paranoid: this.paranoid,
limit: this.getLimit(),
offset: this.getOffset()
};
const limit = this.getLimit();
if (!_.isUndefined(limit)) {
params = _.assign(params, { limit });
}
const offset = this.getOffset();
if (!_.isUndefined(offset)) {
params = _.assign(params, { offset });
}
if (_.isBoolean(this.raw)) {
params = { ...params, ...{ raw: this.raw } };
}
if (_.isArray(this.getAttributes()) && this.getAttributes().length > 0) {
params = _.assign(params, { attributes: this.getAttributes() });
}
let model = this.Models();
if (this.getScopes().length > 0) {
model = model.scope(this.getScopes());
}
const result = await model.findAll(params);
return result;
}
/**
* Retrieve the "count" result of the query.
*
* @return int
*/
async count() {
const params = {
where: this.getWheres(),
include: this.getIncludes(),
order: this.getOrders(),
distinct: true
};
let model = this.Models();
if (this.getScopes().length > 0) {
model = model.scope(this.getScopes());
}
const result = await model.count(params);
return result;
}
/**
* Paginate the given query.
*
* @param {Integer} per_page number item per page
* @param {Integer|null} page page number
*
* @return LengthAwarePaginator
*/
async paginate(per_page = null, page = null) {
if (!_.isNil(per_page)) {
per_page = parseInt(per_page);
} else {
if (Request.has('per_page')) {
per_page = parseInt(Request.get('per_page'));
} else {
per_page = 20;
}
}
if (!_.isNil(page)) {
page = parseInt(page);
} else {
if (Request.has('page')) {
page = parseInt(Request.get('page'));
} else {
page = 1;
}
}
let params = {
offset: (page - 1) * per_page,
limit: per_page,
where: this.getWheres(),
include: this.getIncludes(),
order: this.getOrders(),
distinct: true,
paranoid: this.paranoid
};
if (_.isBoolean(this.raw)) {
params = { ...params, ...{ raw: this.raw } };
}
if (_.isArray(this.getAttributes()) && this.getAttributes().length > 0) {
params = _.assign(params, { attributes: this.getAttributes() });
}
let model = this.Models();
if (this.getScopes().length > 0) {
model = model.scope(this.getScopes());
}
const result = await model.findAndCountAll(params);
const paginator = new LengthAwarePaginator(result.rows, result.count, per_page, page);
return paginator;
}
async fromPagination(pagination) {
const paginationObj = pagination.getData();
paginationObj.operation.forEach(
function(op) {
this[op.type](...op.content);
}.bind(this)
);
return pagination.result(this.paginate.bind(this));
}
/**
* Add a basic "WHERE" clause to the query.
*
* @param {String} column Column name
* @param {mixed} operator Operator or Value
* @param {mixed} value Value
*
* @return this
*/
where(...args) {
if (args.length === 1) {
let raw = false;
if (args[0].constructor) {
if (args[0].constructor.name === 'Where') {
raw = true;
} else if (args[0].constructor.name === 'Literal') {
raw = true;
}
}
if (raw) {
this.builder.where.apply(this.builder, [...args]);
} else {
const callable = args[0];
const builder = new QueryBuilder();
const query = callable(builder);
this.builder.scopeQuery.apply(this.builder, [query]);
}
} else {
this.builder.where.apply(this.builder, [...args]);
}
return this;
}
/**
* Add an "OR WHERE" clause to the query.
*
* @param {String} column Column name
* @param {String|null} operator Operator or Value
* @param {mixed} value Value
*
* @return this
*/
orWhere(...args) {
this.builder.orWhere.apply(this.builder, [...args]);
return this;
}
/**
* Add an "WHERE IN" clause to the query.
*
* @param {String} column Column name
* @param {Array} value Array of values
*
* @return this
*/
whereIn(column, value) {
this.builder.whereIn(column, value);
return this;
}
/**
* Add an "OR WHERE IN" clause to the query.
*
* @param {String} column Column name
* @param {Array} value Array of values
*
* @return this
*/
orWhereIn(column, value) {
this.builder.orWhereIn(column, value);
return this;
}
/**
* Add an "WHERE NOT IN" clause to the query.
*
* @param {String} column Column name
* @param array value
*
* @return this
*/
whereNotIn(column, value) {
this.builder.whereNotIn(column, value);
return this;
}
/**
* Add a basic where clause with relation to the query.
*
* @param {String} relation Relation
* @param {Callable} callable Callback
*
* @return this
*/
whereHas(relation, callable, options) {
let builder = new QueryBuilder();
builder = callable(builder);
this.builder.whereHas.apply(this.builder, [relation, builder, options]);
return this;
}
/**
* include another table that has many to many relationship with it
*
* @param {String} relation Relation
* @param {Callable} callable Callback
*
* @return this
*/
includeThroughWhere(relation, callable) {
let builder = new QueryBuilder();
builder = callable(builder);
this.builder.whereHas.apply(this.builder, [relation, builder]);
return this;
}
/**
* Alias to set the "offset" value of the query.
*
* @param {Integer} value Number of record to skip
*
* @return self
*/
skip(offset) {
this.builder.skip(offset);
return this;
}
/**
* Alias to set the "limit" value of the query.
*
* @param {Integer} value Number of record to take
*
* @return this
*/
take(limit) {
this.builder.take(limit);
return this;
}
/**
* Add an "order by" clause to the query.
*
* @param {String} column Column name to order
* @param {String} direction [ASC|DESC]
*
* @return this
*/
orderBy(...args) {
let model;
let field;
let direction = 'ASC';
if (args.length === 2) {
[field, direction] = args;
this.builder.orderBy(field, direction);
}
if (args.length === 3) {
[model, field, direction] = args;
this.builder.orderBy(model, field, direction);
}
if (args.length === 1) {
[field] = args;
this.builder.orderBy(field);
}
return this;
}
/**
* Add an "GROUP BY" clause to the query.
*
* @param {String} column Column to group
*
* @return self
*/
groupBy(column) {
this.builder.groupBy(column);
return this;
}
/**
* Extract order param from request and apply the rule
*
* @param {Array} fields Supported fields to order
*
* @return Repository
*/
applyOrderFromRequest(fields = [], functions = {}) {
if (Request.has('sort') && Request.get('sort') !== '') {
const orderBy = Request.get('sort').split(',');
orderBy.forEach(field => {
let direction = 'ASC';
if (field.charAt(0) === '-') {
direction = 'DESC';
field = field.slice(1);
}
if (field.charAt(0) === '+') {
field = field.slice(1);
}
if (fields.length === 0 || (fields.length > 0 && _.includes(fields, field))) {
// custom functions to be given aligned with field name
if (typeof functions[field] !== 'undefined') {
functions[field](direction);
} else {
this.orderBy(field, direction);
}
}
});
}
return this;
}
/**
* Extract search param from request and apply the rule
*
* @param {Array} fields Supported fields to search
*
* @return Repository
*/
applySearchFromRequest(fields, match = null) {
if (Request.has('search') && Request.get('search') !== '') {
if (_.isNull(match)) {
match = `%${Request.get('search')}%`;
}
this.where(function(q) {
_.forEach(fields, field => {
q.orWhere(field, 'like', match);
});
return q;
});
}
return this;
}
/**
* Extract constraints param from request and apply the rule
* when supplied custom, it will check from the custom object for its implementation function instead of default where
*
* @param {Object} custom object consisting key of entity type with custom implementation as the value
*
* @return Repository
*/
applyConstraintsFromRequest(custom = {}) {
if (Request.has('constraints') || Request.get('constraints') !== '') {
const data = Request.get('constraints', {});
let constraints = {};
if (typeof data === 'object') {
constraints = Request.get('constraints');
} else {
constraints = JSON.parse(Request.get('constraints'));
}
for (const key in constraints) {
if (typeof custom[key] !== 'undefined') {
custom[key](constraints[key]);
} else {
this.where(key, constraints[key]);
}
}
return this;
}
}
/**
* Begin querying a model with eager loading.
*
* @param {Array|String} relation Relation name
*
* @return this
*/
with(...args) {
this.builder.with.apply(this.builder, args);
return this;
}
/**
* Include deleted records
*
* @return this
*/
withTrashed() {
this.paranoid = false;
return this;
}
/**
* Add scope to the query
*
* @param {String} scope Scope to include
*
* @return this
*/
withScope(scope) {
this.builder.withScope(scope);
return this;
}
/**
* Set the columns to be selected.
*
* @param {Array|mixed} columns Columns to select
*
* @return this
*/
select(columns) {
this.builder.select.apply(this.builder, [columns]);
return this;
}
/**
* Get the sequilize where condition
*
* @return Object
*/
getWheres() {
return this.builder.buildWhereQuery();
}
/**
* Get the limit value of the builder
*
* @return int
*/
getLimit() {
return this.builder.limit;
}
/**
* Get the offset value of the builder
*
* @return int
*/
getOffset() {
return this.builder.offset;
}
/**
* Get the orders value of the builder
*
* @return array
*/
getOrders() {
return this.builder.orders;
}
/**
* Get the group value of the builder
*
* @return array
*/
getGroup() {
return this.builder.group;
}
/**
* Get the includes value of the builder
*
* @return array
*/
getIncludes() {
return this.builder.includes;
}
/**
* Get the scopes value of the builder
*
* @return array
*/
getScopes() {
return this.builder.scopes;
}
/**
* Get the attributes value of the builder
*
* @return array
*/
getAttributes() {
return this.builder.attributes;
}
}