rxdb
Version:
A local-first realtime NoSQL Database for JavaScript applications - https://rxdb.info/
579 lines (507 loc) • 16.9 kB
text/typescript
/**
* this is based on
* @link https://github.com/aheckmann/mquery/blob/master/lib/mquery.js
*/
import {
isObject,
merge
} from './mquery-utils.ts';
import {
newRxTypeError,
newRxError
} from '../../../rx-error.ts';
import type {
MangoQuery,
MangoQuerySelector,
MangoQuerySortPart,
MangoQuerySortDirection
} from '../../../types/index.d.ts';
declare type MQueryOptions = {
limit?: number;
skip?: number;
sort?: any;
};
export class NoSqlQueryBuilderClass<DocType> {
public options: MQueryOptions = {};
public _conditions: MangoQuerySelector<DocType> = {};
public _fields: any = {};
private _distinct: any;
/**
* MQuery constructor used for building queries.
*
* ####Example:
* var query = new MQuery({ name: 'mquery' });
* query.where('age').gte(21).exec(callback);
*
*/
constructor(
mangoQuery?: MangoQuery<DocType>,
public _path?: any
) {
if (mangoQuery) {
const queryBuilder: NoSqlQueryBuilder<DocType> = this as any;
if (mangoQuery.selector) {
queryBuilder.find(mangoQuery.selector);
}
if (mangoQuery.limit) {
queryBuilder.limit(mangoQuery.limit);
}
if (mangoQuery.skip) {
queryBuilder.skip(mangoQuery.skip);
}
if (mangoQuery.sort) {
mangoQuery.sort.forEach(s => queryBuilder.sort(s));
}
}
}
/**
* Specifies a `path` for use with chaining.
*/
where(_path: string, _val?: MangoQuerySelector<DocType>): NoSqlQueryBuilder<DocType> {
if (!arguments.length) return this as any;
const type = typeof arguments[0];
if ('string' === type) {
this._path = arguments[0];
if (2 === arguments.length) {
(this._conditions as any)[this._path] = arguments[1];
}
return this as any;
}
if ('object' === type && !Array.isArray(arguments[0])) {
return this.merge(arguments[0]);
}
throw newRxTypeError('MQ1', {
path: arguments[0]
});
}
/**
* Specifies the complementary comparison value for paths specified with `where()`
* ####Example
* User.where('age').equals(49);
*/
equals(val: any): NoSqlQueryBuilder<DocType> {
this._ensurePath('equals');
const path = this._path;
(this._conditions as any)[path] = val;
return this as any;
}
/**
* Specifies the complementary comparison value for paths specified with `where()`
* This is alias of `equals`
*/
eq(val: any): NoSqlQueryBuilder<DocType> {
this._ensurePath('eq');
const path = this._path;
(this._conditions as any)[path] = val;
return this as any;
}
/**
* Specifies arguments for an `$or` condition.
* ####Example
* query.or([{ color: 'red' }, { status: 'emergency' }])
*/
or(array: any[]): NoSqlQueryBuilder<DocType> {
const or = this._conditions.$or || (this._conditions.$or = []);
if (!Array.isArray(array)) array = [array];
or.push.apply(or, array);
return this as any;
}
/**
* Specifies arguments for a `$nor` condition.
* ####Example
* query.nor([{ color: 'green' }, { status: 'ok' }])
*/
nor(array: any[]): NoSqlQueryBuilder<DocType> {
const nor = this._conditions.$nor || (this._conditions.$nor = []);
if (!Array.isArray(array)) array = [array];
nor.push.apply(nor, array);
return this as any;
}
/**
* Specifies arguments for a `$and` condition.
* ####Example
* query.and([{ color: 'green' }, { status: 'ok' }])
* @see $and http://docs.mongodb.org/manual/reference/operator/and/
*/
and(array: any[]): NoSqlQueryBuilder<DocType> {
const and = this._conditions.$and || (this._conditions.$and = []);
if (!Array.isArray(array)) array = [array];
and.push.apply(and, array);
return this as any;
}
/**
* Specifies a `$mod` condition
*/
mod(_path: string, _val: number): NoSqlQueryBuilder<DocType> {
let val;
let path;
if (1 === arguments.length) {
this._ensurePath('mod');
val = arguments[0];
path = this._path;
} else if (2 === arguments.length && !Array.isArray(arguments[1])) {
this._ensurePath('mod');
val = (arguments as any).slice();
path = this._path;
} else if (3 === arguments.length) {
val = (arguments as any).slice(1);
path = arguments[0];
} else {
val = arguments[1];
path = arguments[0];
}
const conds = (this._conditions as any)[path] || ((this._conditions as any)[path] = {});
conds.$mod = val;
return this as any;
}
/**
* Specifies an `$exists` condition
* ####Example
* // { name: { $exists: true }}
* Thing.where('name').exists()
* Thing.where('name').exists(true)
* Thing.find().exists('name')
*/
exists(_path: string, _val: number): NoSqlQueryBuilder<DocType> {
let path;
let val;
if (0 === arguments.length) {
this._ensurePath('exists');
path = this._path;
val = true;
} else if (1 === arguments.length) {
if ('boolean' === typeof arguments[0]) {
this._ensurePath('exists');
path = this._path;
val = arguments[0];
} else {
path = arguments[0];
val = true;
}
} else if (2 === arguments.length) {
path = arguments[0];
val = arguments[1];
}
const conds = (this._conditions as any)[path] || ((this._conditions as any)[path] = {});
conds.$exists = val;
return this as any;
}
/**
* Specifies an `$elemMatch` condition
* ####Example
* query.elemMatch('comment', { author: 'autobot', votes: {$gte: 5}})
* query.where('comment').elemMatch({ author: 'autobot', votes: {$gte: 5}})
* query.elemMatch('comment', function (elem) {
* elem.where('author').equals('autobot');
* elem.where('votes').gte(5);
* })
* query.where('comment').elemMatch(function (elem) {
* elem.where({ author: 'autobot' });
* elem.where('votes').gte(5);
* })
*/
elemMatch(_path: string, _criteria: any): NoSqlQueryBuilder<DocType> {
if (null === arguments[0])
throw newRxTypeError('MQ2');
let fn;
let path;
let criteria;
if ('function' === typeof arguments[0]) {
this._ensurePath('elemMatch');
path = this._path;
fn = arguments[0];
} else if (isObject(arguments[0])) {
this._ensurePath('elemMatch');
path = this._path;
criteria = arguments[0];
} else if ('function' === typeof arguments[1]) {
path = arguments[0];
fn = arguments[1];
} else if (arguments[1] && isObject(arguments[1])) {
path = arguments[0];
criteria = arguments[1];
} else
throw newRxTypeError('MQ2');
if (fn) {
criteria = new NoSqlQueryBuilderClass;
fn(criteria);
criteria = criteria._conditions;
}
const conds = (this._conditions as any)[path] || ((this._conditions as any)[path] = {});
conds.$elemMatch = criteria;
return this as any;
}
/**
* Sets the sort order
* If an object is passed, values allowed are 'asc', 'desc', 'ascending', 'descending', 1, and -1.
* If a string is passed, it must be a space delimited list of path names.
* The sort order of each path is ascending unless the path name is prefixed with `-` which will be treated as descending.
* ####Example
* query.sort({ field: 'asc', test: -1 });
* query.sort('field -test');
* query.sort([['field', 1], ['test', -1]]);
*/
sort(arg: any): NoSqlQueryBuilder<DocType> {
if (!arg) return this as any;
let len;
const type = typeof arg;
// .sort([['field', 1], ['test', -1]])
if (Array.isArray(arg)) {
len = arg.length;
for (let i = 0; i < arg.length; ++i) {
_pushArr(this.options, arg[i][0], arg[i][1]);
}
return this as any;
}
// .sort('field -test')
if (1 === arguments.length && 'string' === type) {
arg = arg.split(/\s+/);
len = arg.length;
for (let i = 0; i < len; ++i) {
let field = arg[i];
if (!field) continue;
const ascend = '-' === field[0] ? -1 : 1;
if (ascend === -1) field = field.substring(1);
push(this.options, field, ascend);
}
return this as any;
}
// .sort({ field: 1, test: -1 })
if (isObject(arg)) {
const keys = Object.keys(arg);
keys.forEach(field => push(this.options, field, arg[field]));
return this as any;
}
throw newRxTypeError('MQ3', {
args: arguments
});
}
/**
* Merges another MQuery or conditions object into this one.
*
* When a MQuery is passed, conditions, field selection and options are merged.
*
*/
merge(source: any): NoSqlQueryBuilder<DocType> {
if (!source) {
return this as any;
}
if (!canMerge(source)) {
throw newRxTypeError('MQ4', {
source
});
}
if (source instanceof NoSqlQueryBuilderClass) {
// if source has a feature, apply it to ourselves
if (source._conditions)
merge(this._conditions, source._conditions);
if (source._fields) {
if (!this._fields) this._fields = {};
merge(this._fields, source._fields);
}
if (source.options) {
if (!this.options) this.options = {};
merge(this.options, source.options);
}
if (source._distinct)
this._distinct = source._distinct;
return this as any;
}
// plain object
merge(this._conditions, source);
return this as any;
}
/**
* Finds documents.
* ####Example
* query.find()
* query.find({ name: 'Burning Lights' })
*/
find(criteria: any): NoSqlQueryBuilder<DocType> {
if (canMerge(criteria)) {
this.merge(criteria);
}
return this as any;
}
/**
* Make sure _path is set.
*
* @param {String} method
*/
_ensurePath(method: any) {
if (!this._path) {
throw newRxError('MQ5', {
method
});
}
}
toJSON(): {
query: MangoQuery<DocType>;
path?: string;
} {
const query: MangoQuery<DocType> = {
selector: this._conditions,
};
if (this.options.skip) {
query.skip = this.options.skip;
}
if (this.options.limit) {
query.limit = this.options.limit;
}
if (this.options.sort) {
query.sort = mQuerySortToRxDBSort(this.options.sort);
}
return {
query,
path: this._path
};
}
}
export function mQuerySortToRxDBSort<DocType>(
sort: { [k: string]: 1 | -1; }
): MangoQuerySortPart<DocType>[] {
return Object.entries(sort).map(([k, v]) => {
const direction: MangoQuerySortDirection = v === 1 ? 'asc' : 'desc';
const part: MangoQuerySortPart<DocType> = { [k]: direction } as any;
return part;
});
}
/**
* Because some prototype-methods are generated,
* we have to define the type of NoSqlQueryBuilder here
*/
export interface NoSqlQueryBuilder<DocType = any> extends NoSqlQueryBuilderClass<DocType> {
maxScan: ReturnSelfNumberFunction<DocType>;
batchSize: ReturnSelfNumberFunction<DocType>;
limit: ReturnSelfNumberFunction<DocType>;
skip: ReturnSelfNumberFunction<DocType>;
comment: ReturnSelfFunction<DocType>;
gt: ReturnSelfFunction<DocType>;
gte: ReturnSelfFunction<DocType>;
lt: ReturnSelfFunction<DocType>;
lte: ReturnSelfFunction<DocType>;
ne: ReturnSelfFunction<DocType>;
in: ReturnSelfFunction<DocType>;
nin: ReturnSelfFunction<DocType>;
all: ReturnSelfFunction<DocType>;
regex: ReturnSelfFunction<DocType>;
size: ReturnSelfFunction<DocType>;
}
declare type ReturnSelfFunction<DocType> = (v: any) => NoSqlQueryBuilder<DocType>;
declare type ReturnSelfNumberFunction<DocType> = (v: number | null) => NoSqlQueryBuilder<DocType>;
/**
* limit, skip, maxScan, batchSize, comment
*
* Sets these associated options.
*
* query.comment('feed query');
*/
export const OTHER_MANGO_ATTRIBUTES = ['limit', 'skip', 'maxScan', 'batchSize', 'comment'];
OTHER_MANGO_ATTRIBUTES.forEach(function (method) {
(NoSqlQueryBuilderClass.prototype as any)[method] = function (v: any) {
this.options[method] = v;
return this;
};
});
/**
* gt, gte, lt, lte, ne, in, nin, all, regex, size, maxDistance
*
* Thing.where('type').nin(array)
*/
export const OTHER_MANGO_OPERATORS = [
'gt', 'gte', 'lt', 'lte', 'ne',
'in', 'nin', 'all', 'regex', 'size'
];
OTHER_MANGO_OPERATORS.forEach(function ($conditional) {
(NoSqlQueryBuilderClass.prototype as any)[$conditional] = function () {
let path;
let val;
if (1 === arguments.length) {
this._ensurePath($conditional);
val = arguments[0];
path = this._path;
} else {
val = arguments[1];
path = arguments[0];
}
const conds = this._conditions[path] === null || typeof this._conditions[path] === 'object' ?
this._conditions[path] :
(this._conditions[path] = {});
if ($conditional === 'regex') {
if (val instanceof RegExp) {
throw newRxError('QU16', {
field: path,
query: this._conditions,
});
}
if (typeof val === 'string') {
conds['$' + $conditional] = val;
} else {
conds['$' + $conditional] = val.$regex;
if (val.$options) {
conds.$options = val.$options;
}
}
} else {
conds['$' + $conditional] = val;
}
return this;
};
});
function push(opts: any, field: string, value: any) {
if (Array.isArray(opts.sort)) {
throw newRxTypeError('MQ6', {
opts,
field,
value
});
}
if (value && value.$meta) {
const sort = opts.sort || (opts.sort = {});
sort[field] = {
$meta: value.$meta
};
return;
}
const val = String(value || 1).toLowerCase();
if (!/^(?:ascending|asc|descending|desc|1|-1)$/.test(val)) {
if (Array.isArray(value)) value = '[' + value + ']';
throw newRxTypeError('MQ7', {
field,
value
});
}
// store `sort` in a sane format
const s = opts.sort || (opts.sort = {});
const valueStr = value.toString()
.replace('asc', '1')
.replace('ascending', '1')
.replace('desc', '-1')
.replace('descending', '-1');
s[field] = parseInt(valueStr, 10);
}
function _pushArr(opts: any, field: string, value: any) {
opts.sort = opts.sort || [];
if (!Array.isArray(opts.sort)) {
throw newRxTypeError('MQ8', {
opts,
field,
value
});
}
/* const valueStr = value.toString()
.replace('asc', '1')
.replace('ascending', '1')
.replace('desc', '-1')
.replace('descending', '-1');*/
opts.sort.push([field, value]);
}
/**
* Determines if `conds` can be merged using `mquery().merge()`
*/
export function canMerge(conds: any): boolean {
return conds instanceof NoSqlQueryBuilderClass || isObject(conds);
}
export function createQueryBuilder<DocType>(query?: MangoQuery<DocType>, path?: any): NoSqlQueryBuilder<DocType> {
return new NoSqlQueryBuilderClass(query, path) as NoSqlQueryBuilder<DocType>;
}