src/builder.js
import { escape, formatDate } from "./grammar";
function regexHasFlags(re) {
if (typeof re.flags !== "undefined") {
return re.flags.length > 0;
}
return !re.toString().endsWith("/");
}
/**
* Expression is used to build filtering expressions, like those used in WHERE
* clauses. It can be used for fluent and safe building of queries using
* untrusted input.
*
* @example
* e => e
* .field('host').equals.value('ares.peet.io')
* .or
* .field('host').matches(/example\.com$/)
* .or
* .expr(e => e
* .field('country').equals.value('US')
* .and
* .field('state').equals.value('WA'));
*
* // Generates:
* // "host" = 'ares.peet.io' OR "host" ~= /example\.com$/ OR \
* // ("county" = 'US' AND "state" = 'WA')
*/
export class Expression {
constructor() {
this._query = [];
}
/**
* Inserts a tag reference into the expression; the name will be
* automatically escaped.
* @param name
* @return
*/
tag(name) {
this.field(name);
return this;
}
/**
* Inserts a field reference into the expression; the name will be
* automatically escaped.
* @param name
* @return
*/
field(name) {
this._query.push(escape.quoted(name));
return this;
}
/**
* Inserts a subexpression; invokes the function with a new expression
* that can be chained on.
* @param fn
* @return
* @example
* e.field('a').equals.value('b')
* .or.expr(e =>
* e.field('b').equals.value('b')
* .and.field('a').equals.value('c'))
* .toString()
* // "a" = 'b' OR ("b" = 'b' AND "a" = 'c')
*/
exp(fn) {
this._query.push("(" + fn(new Expression()).toString() + ")");
return this;
}
/**
* Value chains on a value to the expression.
*
* - Numbers will be inserted verbatim
* - Strings will be escaped and inserted
* - Booleans will be inserted correctly
* - Dates will be formatted and inserted correctly, including INanoDates.
* - Regular expressions will be inserted correctly, however an error will
* be thrown if they contain flags, as regex flags do not work in Influx
* - Otherwise we'll try to call `.toString()` on the value, throwing
* if we cannot do so.
*
* @param value
* @return
*/
value(value) {
switch (typeof value) {
case "number":
this._query.push(value.toString());
return this;
case "string":
this._query.push(escape.stringLit(value));
return this;
case "boolean":
this._query.push(value ? "TRUE" : "FALSE");
return this;
default:
if (value instanceof Date) {
this._query.push(formatDate(value));
return this;
}
if (value instanceof RegExp) {
if (regexHasFlags(value)) {
throw new Error("Attempted to query using a regex with flags, " +
"but Influx doesn't support flags in queries.");
}
this._query.push("/" + value.source + "/");
return this;
}
if (value && typeof value.toString === "function") {
this._query.push(value.toString());
return this;
}
throw new Error("node-influx doesn't know how to encode the provided value into a " +
"query. If you think this is a bug, open an issue here: https://git.io/influx-err");
}
}
/**
* Chains on an AND clause to the expression.
*/
get and() {
this._query.push("AND");
return this;
}
/**
* Chains on an OR clause to the expression.
*/
get or() {
this._query.push("OR");
return this;
}
/**
* Chains on a `+` operator to the expression.
*/
get plus() {
this._query.push("+");
return this;
}
/**
* Chains on a `*` operator to the expression.
*/
get times() {
this._query.push("*");
return this;
}
/**
* Chains on a `-` operator to the expression.
*/
get minus() {
this._query.push("-");
return this;
}
/**
* Chains on a `/` operator to the expression.
*/
get div() {
this._query.push("/");
return this;
}
/**
* Chains on a `=` conditional to the expression.
*/
get equals() {
this._query.push("=");
return this;
}
/**
* Chains on a `=~` conditional to the expression to match regexes.
*/
get matches() {
this._query.push("=~");
return this;
}
/**
* Chains on a `!`` conditional to the expression to match regexes.
*/
get doesntMatch() {
this._query.push("!~");
return this;
}
/**
* Chains on a `!=` conditional to the expression.
*/
get notEqual() {
this._query.push("!=");
return this;
}
/**
* Chains on a `>` conditional to the expression.
*/
get gt() {
this._query.push(">");
return this;
}
/**
* Chains on a `>=` conditional to the expression.
*/
get gte() {
this._query.push(">=");
return this;
}
/**
* Chains on a `<` conditional to the expression.
*/
get lt() {
this._query.push("<");
return this;
}
/**
* Chains on a `<=` conditional to the expression.
*/
get lte() {
this._query.push("<=");
return this;
}
/**
* Converts the expression into its InfluxQL representation.
* @return
*/
toString() {
return this._query.join(" ");
}
}
/**
* Measurement creates a reference to a particular measurement. You can
* reference it solely by its name, but you can also specify the retention
* policy and database it lives under.
*
* @example
* m.name('my_measurement') // "my_measurement"
* m.name('my_measurement').policy('one_day') // "one_day"."my_measurement"
* m.name('my_measurement').policy('one_day').db('mydb') // "mydb"."one_day"."my_measurement"
*/
export class Measurement {
constructor() {
this._parts = [null, null, null];
}
/**
* Sets the measurement name.
* @param name
* @return
*/
name(name) {
this._parts[2] = name;
return this;
}
/**
* Sets the retention policy name.
* @param retentionPolicy
* @return
*/
policy(retentionPolicy) {
this._parts[1] = retentionPolicy;
return this;
}
/**
* Sets the database name.
* @param db
* @return
*/
db(db) {
this._parts[0] = db;
return this;
}
/**
* Converts the measurement into its InfluxQL representation.
* @return
* @throws {Error} if a measurement name is not provided
*/
toString() {
if (!this._parts[2]) {
throw new Error(`You must specify a measurement name to query! Got \`${this._parts[2]}\``);
}
return this._parts
.filter((p) => Boolean(p))
.map((p) => escape.quoted(p))
.join(".");
}
}
export function parseMeasurement(q) {
if (typeof q.measurement === "function") {
return q.measurement(new Measurement()).toString();
}
return q.measurement;
}
export function parseWhere(q) {
if (typeof q.where === "function") {
return q.where(new Expression()).toString();
}
return q.where;
}