UNPKG

@goatlab/fluent

Version:

Readable query Interface & API generator for TS and Node

293 lines (292 loc) 8.12 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.filterTemplate = exports.FilterBuilder = exports.WhereBuilder = exports.isFilter = void 0; const nonWhereFields = ['fields', 'order', 'limit', 'skip', 'offset', 'include']; const filterFields = ['where', ...nonWhereFields]; function isFilter(candidate) { if (typeof candidate !== 'object') return false; for (const key in candidate) { if (!filterFields.includes(key)) { return false; } } return true; } exports.isFilter = isFilter; class WhereBuilder { constructor(w) { this.where = w ?? {}; } add(w) { for (const k of Object.keys(w)) { if (k in this.where) { const where = { and: [this.where, w] }; this.where = where; return this; } } this.where = Object.assign(this.where, w); return this; } cast(clause) { return clause; } and(...w) { let clauses = []; w.forEach(where => { clauses = clauses.concat(Array.isArray(where) ? where : [where]); }); return this.add({ and: clauses }); } or(...w) { let clauses = []; w.forEach(where => { clauses = clauses.concat(Array.isArray(where) ? where : [where]); }); return this.add({ or: clauses }); } eq(key, val) { const w = {}; w[key] = val; return this.add(w); } neq(key, val) { const w = {}; w[key] = { neq: val }; return this.add(w); } gt(key, val) { const w = {}; w[key] = { gt: val }; return this.add(w); } gte(key, val) { const w = {}; w[key] = { gte: val }; return this.add(w); } lt(key, val) { const w = {}; w[key] = { lt: val }; return this.add(w); } lte(key, val) { const w = {}; w[key] = { lte: val }; return this.add(w); } inq(key, val) { const w = {}; w[key] = { inq: val }; return this.add(w); } nin(key, val) { const w = {}; w[key] = { nin: val }; return this.add(w); } between(key, val1, val2) { const w = {}; w[key] = { between: [val1, val2] }; return this.add(w); } exists(key, val) { const w = {}; w[key] = { exists: !!val || val == null }; return this.add(w); } impose(where) { if (!this.where) { this.where = where || {}; } else { this.add(where); } return this; } like(key, val) { const w = {}; w[key] = { like: val }; return this.add(w); } nlike(key, val) { const w = {}; w[key] = { nlike: val }; return this.add(w); } ilike(key, val) { const w = {}; w[key] = { ilike: val }; return this.add(w); } nilike(key, val) { const w = {}; w[key] = { nilike: val }; return this.add(w); } regexp(key, val) { const w = {}; w[key] = { regexp: val }; return this.add(w); } build() { return this.where; } } exports.WhereBuilder = WhereBuilder; class FilterBuilder { constructor(f) { this.filter = f ?? {}; } limit(limit) { if (!(limit >= 1)) { throw new Error(`Limit ${limit} must a positive number`); } this.filter.limit = limit; return this; } offset(offset) { this.filter.offset = offset; return this; } skip(skip) { return this.offset(skip); } fields(...f) { if (!this.filter.fields) { this.filter.fields = {}; } else if (Array.isArray(this.filter.fields)) { this.filter.fields = this.filter.fields.reduce((prev, current) => ({ ...prev, [current]: true }), {}); } 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}`); } } order(...o) { if (!this.filter.order) { this.filter.order = []; } o.forEach(order => { if (typeof order === 'string') { this.validateOrder(order); if (!order.endsWith(' ASC') && !order.endsWith(' DESC')) { order += ' ASC'; } this.filter.order.push(order); return this; } if (Array.isArray(order)) { order.forEach(this.validateOrder); order = order.map(i => { if (!i.endsWith(' ASC') && !i.endsWith(' DESC')) { i += ' ASC'; } return i; }); this.filter.order = this.filter.order.concat(order); return this; } for (const i in order) { this.filter.order.push(`${i} ${order[i]}`); } }); return this; } 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; } where(w) { this.filter.where = w; return this; } impose(constraint) { if (!this.filter) { if (!isFilter(constraint)) { constraint = { where: constraint }; } this.filter = constraint || {}; } else { if (isFilter(constraint)) { 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; } build() { return this.filter; } } exports.FilterBuilder = FilterBuilder; function getDeepProperty(value, path) { const props = path.split('.'); for (const p of props) { value = value[p]; if (value == null) { return null; } } return value; } 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}`); } }; } exports.filterTemplate = filterTemplate;