lux-framework
Version:
Build scalable, Node.js-powered REST APIs with almost no code.
504 lines (402 loc) • 10.5 kB
JavaScript
// @flow
import { camelize } from 'inflection';
import entries from '../../../utils/entries';
import uniq from '../../../utils/uniq';
import type Model from '../model';
import scopesFor from './utils/scopes-for';
import formatSelect from './utils/format-select';
import { runQuery, createRunner } from './runner';
/**
* @class Query
* @extends Promise
* @private
*/
class Query<+T: any> extends Promise {
/**
* @private
*/
model: Class<Model>;
/**
* @private
*/
isFind: boolean;
/**
* @private
*/
snapshots: Array<Array<any>>;
/**
* @private
*/
collection: boolean;
/**
* @private
*/
shouldCount: boolean;
/**
* @private
*/
relationships: Object;
constructor(model: Class<Model>) {
let resolve;
let reject;
super((res, rej) => {
resolve = res;
reject = rej;
});
createRunner(this, {
resolve,
reject
});
Object.defineProperties(this, {
model: {
value: model,
writable: false,
enumerable: false,
configurable: false
},
collection: {
value: true,
writable: true,
enumerable: false,
configurable: false
},
snapshots: {
value: [],
writable: true,
enumerable: false,
configurable: false
},
shouldCount: {
value: false,
writable: true,
enumerable: false,
configurable: false
},
relationships: {
value: {},
writable: true,
enumerable: false,
configurable: false
}
});
Object.defineProperties(this, scopesFor(this));
}
// $FlowIgnore
static get [Symbol.species]() {
return Promise;
}
all(): this {
return this;
}
not(conditions: Object = {}): this {
return this.where(conditions, true);
}
find(primaryKey: any): this {
Object.assign(this, {
isFind: true,
collection: false
});
this.where({
[this.model.primaryKey]: primaryKey
});
if (!this.shouldCount) {
this.limit(1);
}
return this;
}
page(num: number): this {
if (this.shouldCount) {
return this;
}
let limit = this.snapshots.find(([name]) => name === 'limit');
if (limit) {
[, limit] = limit;
}
if (typeof limit !== 'number') {
limit = 25;
}
this.limit(limit);
return this.offset(Math.max(parseInt(num, 10) - 1, 0) * limit);
}
limit(amount: number): this {
if (!this.shouldCount) {
this.snapshots.push(['limit', amount]);
}
return this;
}
order(attr: string, direction: string = 'ASC'): this {
if (!this.shouldCount) {
const columnName = this.model.columnNameFor(attr);
if (columnName) {
this.snapshots = this.snapshots
.filter(([method]) => method !== 'orderByRaw')
.concat([
['orderByRaw', uniq([columnName, this.model.primaryKey])
.map(key => `${this.model.tableName}.${key} ${direction}`)
.join(', ')
]
]);
}
}
return this;
}
where(conditions: Object = {}, not: boolean = false): this {
const {
model: {
tableName
}
} = this;
const where = entries(conditions).reduce((obj, condition) => {
let [key, value] = condition;
const columnName = this.model.columnNameFor(key);
if (columnName) {
key = `${tableName}.${columnName}`;
if (typeof value === 'undefined') {
value = null;
}
if (Array.isArray(value)) {
if (value.length === 1) {
return {
...obj,
[key]: value[0]
};
}
this.snapshots.push([
not ? 'whereNotIn' : 'whereIn',
[key, value]
]);
} else if (value === null) {
this.snapshots.push([
not ? 'whereNotNull' : 'whereNull',
[key]
]);
} else {
return {
...obj,
[key]: value
};
}
}
return obj;
}, {});
if (Object.keys(where).length) {
this.snapshots.push([not ? 'whereNot' : 'where', where]);
}
return this;
}
whereBetween(conditions: Object, not: boolean = false): this {
const {
model: {
tableName
}
} = this;
entries(conditions).forEach((condition) => {
let [key] = condition;
const [, value] = condition;
const columnName = this.model.columnNameFor(key);
if (columnName) {
key = `${tableName}.${columnName}`;
if (Array.isArray(value)) {
this.snapshots.push([
`where${not ? 'NotBetween' : 'Between'}`,
[key, value]
]);
}
}
});
return this;
}
whereRaw(query: string, bindings: Array<any> = []): this {
this.snapshots.push(['whereRaw', [query, bindings]]);
return this;
}
first(): this {
if (!this.shouldCount) {
const willSort = this.snapshots.some(
([method]) => method === 'orderByRaw'
);
this.collection = false;
if (!willSort) {
this.order(this.model.primaryKey, 'ASC');
}
this.limit(1);
}
return this;
}
last(): this {
if (!this.shouldCount) {
const willSort = this.snapshots.some(
([method]) => method === 'orderByRaw'
);
this.collection = false;
if (!willSort) {
this.order(this.model.primaryKey, 'DESC');
}
this.limit(1);
}
return this;
}
count(): Query<number> {
const validName = /^(where(Not)?(In)?)$/g;
Object.assign(this, {
shouldCount: true,
snapshots: [
['count', '* as countAll'],
...this.snapshots.filter(([name]) => validName.test(name))
]
});
return this;
}
offset(amount: number): this {
if (!this.shouldCount) {
this.snapshots.push(['offset', amount]);
}
return this;
}
select(...attrs: Array<string>): this {
if (!this.shouldCount) {
this.snapshots.push(['select', formatSelect(this.model, attrs)]);
}
return this;
}
distinct(...attrs: Array<string>): this {
if (!this.shouldCount) {
this.snapshots.push(['distinct', formatSelect(this.model, attrs)]);
}
return this.select();
}
include(...relationships: Array<Object | string>): this {
let included;
if (!this.shouldCount) {
if (relationships.length === 1 && typeof relationships[0] === 'object') {
included = entries(relationships[0]).reduce((arr, relationship) => {
const [name] = relationship;
const opts = this.model.relationshipFor(name);
let [, attrs] = relationship;
if (opts) {
if (!attrs.length) {
attrs = opts.model.attributeNames;
}
return [...arr, {
name,
attrs,
relationship: opts
}];
}
return arr;
}, []);
} else {
included = relationships.reduce((arr, name) => {
let str = name;
if (typeof str !== 'string') {
str = String(str);
}
const opts = this.model.relationshipFor(str);
if (opts) {
const attrs = opts.model.attributeNames;
return [...arr, {
attrs,
name: str,
relationship: opts
}];
}
return arr;
}, []);
}
const willInclude = included
.filter(opts => {
const { name, relationship } = opts;
let { attrs } = opts;
if (relationship.type === 'hasMany') {
attrs = relationship.through ? attrs : [
...attrs,
camelize(relationship.foreignKey, true)
];
this.relationships[name] = {
attrs,
type: 'hasMany',
model: relationship.model,
through: relationship.through,
foreignKey: relationship.foreignKey
};
return false;
}
return true;
})
.reduce((arr, { name, attrs, relationship }) => {
arr.push([
'includeSelect',
formatSelect(relationship.model, attrs, `${name}.`)
]);
if (relationship.type === 'belongsTo') {
arr.push(['leftOuterJoin', [
relationship.model.tableName,
`${this.model.tableName}.${relationship.foreignKey}`,
'=',
`${relationship.model.tableName}.` +
`${relationship.model.primaryKey}`
]]);
} else if (relationship.type === 'hasOne') {
arr.push(['leftOuterJoin', [
relationship.model.tableName,
`${this.model.tableName}.${this.model.primaryKey}`,
'=',
`${relationship.model.tableName}.${relationship.foreignKey}`
]]);
}
return arr;
}, []);
this.snapshots.push(...willInclude);
}
return this;
}
unscope(...scopes: Array<string>): this {
if (scopes.length) {
const keys = scopes.map(scope => {
if (scope === 'order') {
return 'orderByRaw';
}
return scope;
});
this.snapshots = this.snapshots.filter(([, , scope]) => {
if (typeof scope === 'string') {
return keys.indexOf(scope) < 0;
}
return true;
});
} else {
this.snapshots = this.snapshots.filter(([, , scope]) => !scope);
}
return this;
}
then<U>(
onFulfilled?: (value: T) => Promise<U> | U,
onRejected?: (error: Error) => Promise<U> | U
): Promise<U> {
runQuery(this);
return super.then(onFulfilled, onRejected);
} // eslint-disable-line brace-style
catch<U>(onRejected?: (error: Error) => ?Promise<U> | U): Promise<U> {
runQuery(this);
return super.catch(onRejected);
}
static from(src: any): Query<T> {
const {
model,
snapshots,
collection,
shouldCount,
relationships
} = src;
const dest = Reflect.construct(this, [model]);
Object.assign(dest, {
snapshots,
collection,
shouldCount,
relationships
});
return dest;
}
}
export default Query;
export { RecordNotFoundError } from './errors';