@goatlab/fluent
Version:
Readable query Interface & API generator for TS and Node
490 lines • 13.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FilterBuilder = exports.WhereBuilder = void 0;
exports.isFilter = isFilter;
exports.filterTemplate = filterTemplate;
/* eslint-disable @typescript-eslint/no-explicit-any */
const nonWhereFields = ['fields', 'order', 'limit', 'skip', 'offset', 'include'];
const filterFields = ['where', ...nonWhereFields];
/**
* TypeGuard for Filter
* @param candidate
*/
function isFilter(candidate) {
if (typeof candidate !== 'object') {
return false;
}
for (const key in candidate) {
if (!filterFields.includes(key)) {
return false;
}
}
return true;
}
/**
* A builder for Where object. It provides fluent APIs to add clauses such as
* `and`, `or`, and other operators.
*
* @example
* ```ts
* const whereBuilder = new WhereBuilder();
* const where = whereBuilder
* .eq('a', 1)
* .and({x: 'x'}, {y: {gt: 1}})
* .and({b: 'b'}, {c: {lt: 1}})
* .or({d: 'd'}, {e: {neq: 1}})
* .build();
* ```
*/
class WhereBuilder {
where;
constructor(w) {
this.where = w ?? {};
}
add(w) {
for (const k of Object.keys(w)) {
if (k in this.where) {
// Found conflicting keys, create an `and` operator to join the existing
// conditions with the new one
const where = { and: [this.where, w] };
this.where = where;
return this;
}
}
// Merge the where items
this.where = Object.assign(this.where, w);
return this;
}
/**
* @deprecated
* Starting from TypeScript 3.2, we don't have to cast any more. This method
* should be considered as `deprecated`.
*
* Cast an `and`, `or`, or condition clause to Where
* @param clause - And/Or/Condition clause
*/
cast(clause) {
return clause;
}
/**
* Add an `and` clause.
* @param w - One or more where objects
*/
and(...w) {
let clauses = [];
w.forEach(where => {
clauses = clauses.concat(Array.isArray(where) ? where : [where]);
});
return this.add({ and: clauses });
}
/**
* Add an `or` clause.
* @param w - One or more where objects
*/
or(...w) {
let clauses = [];
w.forEach(where => {
clauses = clauses.concat(Array.isArray(where) ? where : [where]);
});
return this.add({ or: clauses });
}
/**
* Add an `=` condition
* @param key - Property name
* @param val - Property value
*/
eq(key, val) {
const w = {};
w[key] = val;
return this.add(w);
}
/**
* Add a `!=` condition
* @param key - Property name
* @param val - Property value
*/
neq(key, val) {
const w = {};
w[key] = { neq: val };
return this.add(w);
}
/**
* Add a `>` condition
* @param key - Property name
* @param val - Property value
*/
gt(key, val) {
const w = {};
w[key] = { gt: val };
return this.add(w);
}
/**
* Add a `>=` condition
* @param key - Property name
* @param val - Property value
*/
gte(key, val) {
const w = {};
w[key] = { gte: val };
return this.add(w);
}
/**
* Add a `<` condition
* @param key - Property name
* @param val - Property value
*/
lt(key, val) {
const w = {};
w[key] = { lt: val };
return this.add(w);
}
/**
* Add a `<=` condition
* @param key - Property name
* @param val - Property value
*/
lte(key, val) {
const w = {};
w[key] = { lte: val };
return this.add(w);
}
/**
* Add a `inq` condition
* @param key - Property name
* @param val - An array of property values
*/
inq(key, val) {
const w = {};
w[key] = { inq: val };
return this.add(w);
}
/**
* Add a `nin` condition
* @param key - Property name
* @param val - An array of property values
*/
nin(key, val) {
const w = {};
w[key] = { nin: val };
return this.add(w);
}
/**
* Add a `between` condition
* @param key - Property name
* @param val1 - Property value lower bound
* @param val2 - Property value upper bound
*/
between(key, val1, val2) {
const w = {};
w[key] = { between: [val1, val2] };
return this.add(w);
}
/**
* Add a `exists` condition
* @param key - Property name
* @param val - Exists or not
*/
exists(key, val) {
const w = {};
w[key] = { exists: !!val || val == null };
return this.add(w);
}
/**
* Add a where object. For conflicting keys with the existing where object,
* create an `and` clause.
* @param where - Where filter
*/
impose(where) {
if (!this.where) {
this.where = where || {};
}
else {
this.add(where);
}
return this;
}
/**
* Add a `like` condition
* @param key - Property name
* @param val - Regexp condition
*/
like(key, val) {
const w = {};
w[key] = { like: val };
return this.add(w);
}
/**
* Add a `nlike` condition
* @param key - Property name
* @param val - Regexp condition
*/
nlike(key, val) {
const w = {};
w[key] = { nlike: val };
return this.add(w);
}
/**
* Add a `ilike` condition
* @param key - Property name
* @param val - Regexp condition
*/
ilike(key, val) {
const w = {};
w[key] = { ilike: val };
return this.add(w);
}
/**
* Add a `nilike` condition
* @param key - Property name
* @param val - Regexp condition
*/
nilike(key, val) {
const w = {};
w[key] = { nilike: val };
return this.add(w);
}
/**
* Add a `regexp` condition
* @param key - Property name
* @param val - Regexp condition
*/
regexp(key, val) {
const w = {};
w[key] = { regexp: val };
return this.add(w);
}
/**
* Get the where object
*/
build() {
return this.where;
}
}
exports.WhereBuilder = WhereBuilder;
/**
* A builder for Filter. It provides fleunt APIs to add clauses such as
* `fields`, `order`, `where`, `limit`, `offset`, and `include`.
*
* @example
* ```ts
* const filterBuilder = new FilterBuilder();
* const filter = filterBuilder
* .fields('id', 'a', 'b')
* .limit(10)
* .offset(0)
* .order(['a ASC', 'b DESC'])
* .where({id: 1})
* .build();
* ```
*/
class FilterBuilder {
filter;
constructor(f) {
this.filter = f ?? {};
}
/**
* Set `limit`
* @param limit - Maximum number of records to be returned
*/
limit(limit) {
if (!(limit >= 1)) {
throw new Error(`Limit ${limit} must a positive number`);
}
this.filter.limit = limit;
return this;
}
/**
* Set `offset`
* @param offset - Offset of the number of records to be returned
*/
offset(offset) {
this.filter.offset = offset;
return this;
}
/**
* Alias to `offset`
* @param skip
*/
skip(skip) {
return this.offset(skip);
}
/**
* Describe what fields to be included/excluded
* @param f - A field name to be included, an array of field names to be
* included, or an Fields object for the inclusion/exclusion
*/
fields(...f) {
if (!this.filter.fields) {
this.filter.fields = {};
}
else if (Array.isArray(this.filter.fields)) {
const fieldsObj = {};
for (const field of this.filter.fields) {
fieldsObj[field] = true;
}
this.filter.fields = fieldsObj;
}
const { fields } = this.filter;
for (const field of f) {
if (Array.isArray(field)) {
field.forEach(i => {
fields[i] = true;
});
}
else if (typeof field === 'string') {
fields[field] = true;
}
else {
Object.assign(fields, field);
}
}
return this;
}
validateOrder(order) {
if (!order.match(/^[^\s]+( (ASC|DESC))?$/)) {
throw new Error(`Invalid order: ${order}`);
}
}
/**
* Describe the sorting order
* @param o - A field name with optional direction, an array of field names,
* or an Order object for the field/direction pairs
*/
order(...o) {
if (!this.filter.order) {
this.filter.order = [];
}
o.forEach(orderItem => {
if (typeof orderItem === 'string') {
this.validateOrder(orderItem);
const finalOrder = !orderItem.endsWith(' ASC') && !orderItem.endsWith(' DESC')
? `${orderItem} ASC`
: orderItem;
this.filter.order.push(finalOrder);
return this;
}
if (Array.isArray(orderItem)) {
orderItem.forEach(this.validateOrder);
const mappedOrder = orderItem.map(i => {
return !i.endsWith(' ASC') && !i.endsWith(' DESC') ? `${i} ASC` : i;
});
this.filter.order = this.filter.order.concat(mappedOrder);
return this;
}
// tslint:disable-next-line: forin
for (const i in orderItem) {
this.filter.order.push(`${i} ${orderItem[i]}`);
}
});
return this;
}
/**
* Declare `include`
* @param i - A relation name, an array of relation names, or an `Inclusion`
* object for the relation/scope definitions
*/
include(...i) {
if (this.filter.include == null) {
this.filter.include = [];
}
for (const include of i) {
if (typeof include === 'string') {
this.filter.include.push({ relation: include });
}
else if (Array.isArray(include)) {
for (const inc of include) {
this.filter.include.push({ relation: inc });
}
}
else {
this.filter.include.push(include);
}
}
return this;
}
/**
* Declare a where clause
* @param w - Where object
*/
where(w) {
this.filter.where = w;
return this;
}
/**
* Add a Filter or Where constraint object. If it is a filter object, create
* an `and` clause for conflicting keys with its where object. For any other
* properties, throw an error. If it's not a Filter, coerce it to a filter,
* and carry out the same logic.
*
* @param constraint - a constraint object to merge with own filter object
*/
impose(constraint) {
if (!this.filter) {
// if constraint is a Where, turn into a Filter
const filterConstraint = !isFilter(constraint)
? { where: constraint }
: constraint;
this.filter = filterConstraint || {};
}
else {
if (isFilter(constraint)) {
// throw error if imposed Filter has non-where fields
for (const key of Object.keys(constraint)) {
if (nonWhereFields.includes(key)) {
throw new Error('merging strategy for selection, pagination, and sorting not implemented');
}
}
}
this.filter.where = isFilter(constraint)
? new WhereBuilder(this.filter.where).impose(constraint.where).build()
: new WhereBuilder(this.filter.where).impose(constraint).build();
}
return this;
}
/**
* Return the filter object
*/
build() {
return this.filter;
}
}
exports.FilterBuilder = FilterBuilder;
/**
* Get nested properties by path
* @param value - Value of an object
* @param path - Path to the property
*/
function getDeepProperty(value, path) {
const props = path.split('.');
let current = value;
for (const p of props) {
current = current[p];
if (current == null) {
return null;
}
}
return current;
}
function filterTemplate(strings, ...keys) {
return function filter(ctx) {
const tokens = [strings[0]];
keys.forEach((key, i) => {
if (typeof key === 'object' ||
typeof key === 'boolean' ||
typeof key === 'number') {
tokens.push(JSON.stringify(key), strings[i + 1]);
return;
}
const value = getDeepProperty(ctx, key);
tokens.push(JSON.stringify(value), strings[i + 1]);
});
const result = tokens.join('');
try {
return JSON.parse(result);
}
catch (_e) {
throw new Error(`Invalid JSON: ${result}`);
}
};
}
//# sourceMappingURL=query.js.map