modules/core/server/models/objection/index.js
/* eslint no-underscore-dangle:0 */
/* jslint node:true, esnext:true */
/* globals LACKEY_PATH */
'use strict';
/*
Copyright 2016 Enigma Marketing Services Limited
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const
DataSources = require(LACKEY_PATH).datasources,
SCli = require(LACKEY_PATH).cli,
Database = DataSources.get('knex', 'default'),
objection = require('objection'),
_ = require('lodash'),
Model = objection.Model,
__MODULE_NAME = 'lackey-cms/modules/core/server/models/objection';
SCli.debug(__MODULE_NAME, 'REQUIRED');
module.exports = Database
.then((knex) => {
return knex
.schema
.dropTableIfExists('objection')
.then(() => {
return knex.schema.createTableIfNotExists('objection', (table) => {
table.increments('id').primary();
table.timestamp('createdAt').notNullable().defaultTo(knex.raw('now()'));
table.timestamp('updatedAt').notNullable().defaultTo(knex.raw('now()'));
table.string('name');
});
})
.then(() => {
/**
* @class
*/
class ObjectionModel extends Model {
static get tableName() {
return 'objection';
}
}
/**
* Handle any error
* @param {Error} outerError
* @param {object} instance
* @returns {Error}
*/
function handleError(outerError, instance) {
return function (error, silent) {
SCli.debug(__MODULE_NAME, 'ERROR', error.message);
SCli.debug(__MODULE_NAME, 'ERROR', outerError.stack);
console.error(error);
console.error(outerError.stack);
if (instance) {
console.error(instance);
}
if (silent) {
throw error;
}
return error;
};
}
/**
* @class
*/
class ObjectionWrapper {
/**
* @param {object} data
*/
constructor(data) {
this._doc = data || {};
}
/**
* @returns {object}
*/
toJSON() {
return JSON.parse(JSON.stringify(this._doc));
}
/**
* Saves instance
* @param {object} options
* @returns {Promise}
*/
save(options) {
SCli.debug(__MODULE_NAME, 'save', this.constructor.model.tableName);
let self = this,
hook = new Error(),
cached = _.cloneDeep(this._doc);
return this
._preSave(options)
.then(() => {
return self._filter();
})
.then(() => {
if (!self._doc.id) {
return SCli
.sql(self.constructor.model
.query()
.insertAndFetch(self._doc)
)
.then((result) => {
SCli.debug(__MODULE_NAME, 'created', self.constructor.model.tableName);
return result;
}, handleError(new Error(), self));
}
self._doc.updatedAt = new Date();
return SCli
.sql(self.constructor.model
.query()
.updateAndFetchById(self.id, self._doc)
)
.then((result) => {
SCli.debug(__MODULE_NAME, 'saved', self.constructor.model.tableName);
return result;
}, handleError(hook, self));
})
.then((data) => {
self._doc = data;
return self._postSave(cached);
})
.then(() => {
return self._populate();
});
}
get id() {
return this._doc.id || null;
}
get name() {
return this._doc.name;
}
set name(name) {
this._doc.name = name;
}
get createdAt() {
return this._doc.createdAt;
}
get updatedAt() {
return this._doc.updatedAt;
}
set createdAt(value) {
this._doc.createdAt = value;
}
set updatedAt(value) {
this._doc.updatedAt = value;
}
_populate() {
this._populated = true;
return Promise.resolve(this);
}
_preSave() {
return Promise.resolve(this);
}
_filter() {
return Promise.resolve(this);
}
_postSave() {
return Promise.resolve(this);
}
diff(data) {
let self = this;
_.merge(self._doc, data);
return true;
}
update(data) {
if (this.diff(data)) {
return this.save();
}
return Promise.resovle(this);
}
remove() {
let self = this;
return SCli
.sql(self.constructor.model
.query()
.where('id', self.id)
.del()
)
.then((result) => result);
}
static get model() {
return ObjectionModel;
}
static get likeables() {
return {};
}
/**
* Creates instance
* @param {Mixed} data
* @returns {Promise} of instance
*/
static create(data) {
SCli.debug(__MODULE_NAME, 'create', this.model.tableName, JSON.stringify(data));
let Self = this;
return (new Self(data)).save();
}
/**
* Gets instance by id
* @param {Number|String} id
* @returns {Promise} of instance or null
*/
static findById(id) {
SCli.debug(__MODULE_NAME, 'findById', this.model.tableName, id);
if (!id) {
return Promise.resolve(null);
}
if (isNaN(id)) {
return Promise.reject();
}
return this.findOneBy('id', id);
}
/**
* Gets instance by id
* @param {Number|String} id
* @returns {Promise} of instance or null
*/
static findBy(field, value) {
if (typeof field !== 'string') {
throw new Error('Field name should be a string');
}
SCli.debug(__MODULE_NAME, 'findBy', this.model.tableName, field, value);
let Self = this,
hook = new Error();
return SCli
.sql(this.model
.query()
.where(field, value)
)
.then((result) => {
SCli.debug(__MODULE_NAME, 'findBy result', this.model.tableName, JSON.stringify(result));
return Promise.all(result.map((data) => Self.factory(data)));
}, (err) => {
handleError(hook)(err, true);
return null;
});
}
/**
* Gets instance by id
* @param {Number|String} id
* @returns {Promise} of instance or null
*/
static findOneBy(field, value) {
if (typeof field !== 'string') {
throw new Error('Field name should be a string');
}
SCli.debug(__MODULE_NAME, 'findOneBy', this.model.tableName, field, value);
let Self = this,
hook = new Error();
return SCli
.sql(this.model
.query()
.where(field, value)
)
.then((result) => {
if (!result || !result.length) {
return null;
}
SCli.debug(__MODULE_NAME, 'findOneBy result', this.model.tableName, JSON.stringify(result[0]));
return Self.factory(result[0]);
}, (err) => {
handleError(hook)(err, true);
return null;
});
}
static findByIds(ids) {
SCli.debug(__MODULE_NAME, 'findByIds', this.model.tableName, ids);
if (!ids || !ids.length) {
return Promise.resolve([]);
}
let Self = this,
hook = new Error();
return SCli
.sql(this.model
.query()
.whereIn('id', ids)
)
.then((result) => {
if (!result) {
return null;
}
return Promise.all(result.map((data) => Self.factory(data)));
}, handleError(hook));
}
/**
* Gets all objects from table (be sure you want to use it)
* @returns {Promise} of array of instances
*/
static find() {
SCli.debug(__MODULE_NAME, 'find', this.model.tableName);
let Self = this;
return SCli
.sql(this.model
.query()
)
.then((results) => {
return Promise.all(results.map((result) => Self.factory(result)));
}, handleError(new Error()));
}
static factory(data) {
return (new this(data))._populate();
}
static list() {
SCli.debug(__MODULE_NAME, 'list', this.model.tableName);
return this.find();
}
static where(cursor, query, operand) {
SCli.debug(__MODULE_NAME, 'where', this.model.tableName, JSON.stringify(query), operand);
let self = this,
fn = operand === 'or' ? 'orWhere' : 'where',
outputQuery = query || {};
Object.keys(outputQuery).forEach((key) => {
if (key === '$or') {
cursor.andWhere(function () {
let inner = this;
outputQuery.$or.forEach((condition, index) => {
if (index === 0) {
self.where(inner, condition);
} else {
self.where(inner, condition, 'or');
}
});
});
}
if (key === '$and') {
cursor.andWhere(function () {
let inner = this;
outputQuery.$and.forEach((condition) => {
self.where(inner, condition);
});
});
} else if (outputQuery[key] === null) {
cursor[operand === 'or' ? 'orWhereNull' : 'whereNull'](key);
} else if (typeof outputQuery[key] === 'object') {
if (Array.isArray(outputQuery[key].$in)) {
cursor[operand === 'or' ? 'orWhereIn' : 'whereIn'](key, outputQuery[key].$in);
} else if (outputQuery[key].$ne) {
cursor[operand === 'or' ? 'orWhereNot' : 'whereNot'](key, outputQuery[key].$ne);
} else if (outputQuery[key].operator === 'like') {
cursor[fn](knex.raw('LOWER("' + key.replace(/"/g, '') + '") LIKE LOWER(?)', outputQuery[key].value));
} else {
cursor[fn](key, outputQuery[key].operator, outputQuery[key].value);
}
} else {
cursor[fn](key, outputQuery[key]);
}
});
return cursor;
}
static count(query) {
SCli.debug(__MODULE_NAME, 'count', this.model.tableName, JSON.stringify(query));
let cursor = this.model
.query()
.count();
if (query) {
cursor = this.where(cursor, query);
}
return SCli
.sql(
cursor
)
.then((result) => {
if (result && result.length) {
return +result[0].count;
}
return -1;
});
}
static queryWithCount(query, populate, options) {
let self = this,
total;
if (query) {
Object.keys(query).forEach((key) => {
query[key] = self.parseLike(query[key]);
});
}
return self
.count(query)
.then((count) => {
total = count;
return self.query(query, populate, options);
})
.then((results) => {
return {
paging: {
limit: options.limit,
offset: options.offset,
sort: options.sort,
total: total,
filters: query
},
data: results
};
});
}
static query(query, populate, options) {
let self = this;
SCli.debug(__MODULE_NAME, 'query', this.model.tableName);
if (!self.model) {
return Promise.reject('This model doesn\'t supply mongo model reference for shared methods');
}
let cursor = self
.model
.query();
if (options) {
if (options.sort) {
Object.keys(options.sort).forEach((key) => {
cursor = cursor.orderBy(key, options.sort[key] >= 0 ? 'asc' : 'desc');
});
}
if (options.offset) {
cursor = cursor.offset(options.offset);
}
if (options.limit) {
cursor = cursor.limit(options.limit);
}
}
cursor = self.where(cursor, query);
return SCli
.sql(cursor)
.then((documents) => {
return Promise
.all(documents.map((doc) => {
return self.factory(doc);
}));
});
}
static parseLike(value) {
if (!value || !value.match) return value;
let matches = value.match(/^(%|)([^%]+)(|%)$/);
if (matches) {
if (matches[1] === '%' && matches[3] === '%') {
return new RegExp(matches[2]);
} else if (matches[3] === '%') {
return new RegExp(matches[2] + '$');
} else if (matches[1] === '%') {
return new RegExp('^' + matches[2]);
}
}
return value;
}
static _preQuery(query, options) {
let amendedQuery = JSON.parse(JSON.stringify(query || {}));
if (this.likeables && options && options.textSearch) {
let likeableKeys = Object.keys(this.likeables);
if (likeableKeys.length) {
let or = [];
if (amendedQuery.$or) {
amendedQuery.$and = amendedQuery.$and || [];
amendedQuery.$and.push({
$or: amendedQuery.$or
});
amendedQuery.$and.push({
$or: or
});
delete amendedQuery.$or;
} else {
amendedQuery.$or = or;
}
likeableKeys
.forEach(key => {
let obj = {};
obj[key] = {
operator: 'like',
value: this.likeables[key] === 'lr' ? ('%' + options.textSearch + '%') : (this.likeables[key] === 'l' ? ('%' + options.textSearch) : (options.textSearch + '%'))
};
or.push(obj);
});
}
}
return Promise.resolve(amendedQuery);
}
/**
* @private
* @param {object} data
* @param {string} query
* @param {object} options
* @returns {Promise}
*/
static _postQuery(data) {
return Promise.resolve(data);
}
/**
* Generates table data
* @param {object} inputQuery
* @param {Array} inputColumns
* @param {object} options
*/
static table(inputQuery, inputColumns, options) {
SCli.debug(__MODULE_NAME, 'table', this.model.tableName, JSON.stringify(inputQuery), JSON.stringify(inputColumns), JSON.stringify(options));
let
query,
columns = inputColumns,
self = this,
columnsArray = columns ? Object.keys(columns).sort((a, b) => {
if (columns[a].ord > columns[b].ord) return 1;
if (columns[a].ord < columns[b].ord) return -1;
return 0;
}) : null,
perPage = (options ? options.perPage : false) || 10,
page = (options ? options.page - 1 : false) || 0,
sort = (options ? options.sort : null),
populate = null,
table = {};
if (!sort) {
sort = {
id: 1
};
}
if (columnsArray) {
populate = {};
columnsArray.forEach((column) => {
if (typeof columns[column] === 'number') {
populate[column] = columns[column];
} else {
populate[column] = 1;
}
});
}
return this
._preQuery(inputQuery, options)
.then(q => {
query = q;
return this.count(q);
})
.then(count => {
let opt = options || {};
if (opt.limit && !opt.nolimit) {
perPage = opt.limit;
}
if (opt.offset && !opt.nolimit) {
//page = Math.floor(opt.offset / perPage) - 1;
}
table.paging = {
total: count,
pages: Math.ceil(count / perPage),
page: page + 1,
perPage: perPage,
offset: page * perPage,
filters: query,
api: self.api,
start: (page + 1) - 3,
finish: (page + 1) + 3,
startNo: (page * perPage) + 1
};
if (sort) {
table.paging.sort = sort;
}
let queryOptions = {};
if (!opt.nolimit) {
queryOptions = {
offset: opt.offset !== undefined ? opt.offset : table.paging.offset,
limit: opt.limit !== undefined ? opt.limit : table.paging.perPage
};
}
if (sort) {
queryOptions.sort = sort;
}
if (opt.textSearch) {
queryOptions.textSearch = options.textSearch;
}
if (opt.taxonomies) {
queryOptions.taxonomies = options.taxonomies;
}
return self.query(query, populate, queryOptions);
})
.then((data) => {
let rows = data.map((content) => {
return content.toJSON();
});
if (!columnsArray) {
columns = {};
columnsArray = [];
rows.forEach((row) => {
Object.keys(row).forEach((field) => {
if (columnsArray.indexOf(field) === -1) {
columnsArray.push(field);
columns[field] = field;
}
});
});
}
columnsArray.forEach((column) => {
let def = columns[column];
if (typeof def === 'string') {
def = columns[column] = {
label: def
};
} else if (typeof def === 'number') {
if (def === -1) {
delete columns[column];
return;
} else {
def = columns[column] = {
label: column
};
}
}
def.label = def.label || column;
def.name = column;
def.type = def.type || 'string';
});
rows = rows.map((row) => {
let formatted;
if (options && options.format === 'table') {
formatted = {
id: row.id,
columns: []
};
columnsArray.forEach((column) => {
let value = row[column],
parse;
if (columns[column].parse) {
try {
parse = new Function('val', columns[column].parse); //eslint-disable-line no-new-func
value = parse(value, row);
} catch (ex) {
console.error(ex);
}
}
if (value && columns[column].link) {
value = {
href: value,
label: columns[column].linkText || value
};
}
if (value && columns[column].date) {
value = {
date: (typeof value === 'string' ? new Date(value) : value).getTime()
};
}
if (!value && columns[column].default) {
value = columns[column].default;
}
if (columns[column].default) {
if (!value || Array.isArray(value) && value.length < 1) {
value = columns[column].default;
}
}
if (value !== undefined) {
if (Array.isArray(value)) {
value = value.join(', ');
}
formatted.columns.push({
value: value,
hide: columns[column].hide || false
});
} else {
formatted.columns.push({});
}
});
} else {
formatted = row;
}
if (options.keepReference) {
formatted.___origial = row;
}
return formatted;
});
let newColumns = [];
columnsArray.forEach((column) => {
if (columns[column].parse) {
columns[column].parse = columns[column].parse.toString();
}
newColumns.push(columns[column]);
});
if (options && options.format === 'table') {
table.columns = newColumns;
table.rows = rows;
} else {
table.data = rows;
}
return table;
})
.then((tableData) => {
return self._postQuery(tableData, inputQuery, options);
});
}
////
/**
* Removes all rows,
* @returns {Promise} of number of delete rows
*/
static removeAll() {
SCli.debug(__MODULE_NAME, 'removeAll', this.model.tableName);
let hook = new Error();
return SCli.sql(this.model
.query()
.delete()
).catch(handleError(hook));
}
bind(fn, args) {
SCli.debug(__MODULE_NAME, 'bind');
let self = this;
return function () {
SCli.debug(__MODULE_NAME, 'bound');
return fn.apply(self, args);
};
}
bindCapture(fn) {
SCli.debug(__MODULE_NAME, 'bindCapture');
let self = this;
return function () {
SCli.debug(__MODULE_NAME, 'bound and captured');
let args = [].slice.call(arguments);
return fn.apply(self, args);
};
}
}
SCli.debug(__MODULE_NAME, 'READY');
return ObjectionWrapper;
});
});