using afBson::BsonIO
** A means to build common Mongo queries with sane objects and methods.
** (And not some incomprehensible mess of nested maps and lists!)
**
** pre>
** syntax: fantom
** query := MongoQ {
** and(
** or( eq("price", 0.99f), eq("price", 1.99f) ),
** or( eq("sale", true), lessThan("qty", 29) )
** )
** }.query
** <pre
class MongoQ {
// this weird class is both the MongoQ AND its own builder!
** The underlying query that's being build up.
Str:Obj? query() {
if (_innerQ != null)
return obj[_innerQ._key] = _innerQ._val
if (_key != null)
return obj[_key] = _val
// if _key is null, then NO query methods have been called - we're empty!
return obj
}
private Str? _key
private Obj? _val
private Bool _not
** Creates a standard MongoQ instance.
new make() {
this._nameHookFn = _defHookFn
this._valueHookFn = _defHookFn
}
** Create a query instance with name / value hooks.
@NoDoc
new makeWithHookFns(|Obj->Str| nameHookFn, |Obj?->Obj?| valueHookFn) {
this._nameHookFn = nameHookFn
this._valueHookFn = valueHookFn
}
// ---- Comparison MongoQ Operators ---------
** Matches values that are equal to the given object.
**
** syntax: fantom
** q.eq("score", 11)
**
** Shorthand notation.
**
** syntax: fantom
** q->score = 11
**
MongoQ eq(Obj name, Obj? value) {
_q._set(name, _valueHookFn(value))
}
** Matches values that are **not** equal to the given object.
**
** Note this also matches documents that do not contain the field.
**
** syntax: fantom
** q.notEq("score", 11)
**
** @see `https://www.mongodb.com/docs/manual/reference/operator/query/ne/`
MongoQ notEq(Obj name, Obj? value) {
_q.op(name, "\$ne", value)
}
** Matches values that equal any one of the given values.
**
** syntax: fantom
** q.in("score", [9, 10, 11])
**
** @see `https://www.mongodb.com/docs/manual/reference/operator/query/in/`
MongoQ in(Obj name, Obj[] values) {
_q.op(name, "\$in", values) // BSON converter is deep!
}
** Matches values that do **not** equal any one of the given values.
**
** Note this also matches documents that do not contain the field.
**
** syntax: fantom
** q.notIn("score", [1, 2, 3])
**
** @see `https://www.mongodb.com/docs/manual/reference/operator/query/nin/`
MongoQ notIn(Obj name, Obj[] values) {
_q.op(name, "\$nin", values) // BSON converter is deep!
}
** Matches values that are greater than the given object.
**
** syntax: fantom
** q.greaterThan("score", 8)
**
** @see `https://www.mongodb.com/docs/manual/reference/operator/query/gt/`
MongoQ greaterThan(Obj name, Obj value) {
_q.op(name, "\$gt", value)
}
** Matches values that are greater than or equal to the given object.
**
** syntax: fantom
** q.greaterThanOrEqTo("score", 8)
**
** @see `https://www.mongodb.com/docs/manual/reference/operator/query/gte/`
MongoQ greaterThanOrEqTo(Obj name, Obj value) {
_q.op(name, "\$gte", value)
}
** Matches values that are less than the given object.
**
** syntax: fantom
** q.lessThan("score", 5)
**
** @see `https://www.mongodb.com/docs/manual/reference/operator/query/gt/`
MongoQ lessThan(Obj name, Obj value) {
_q.op(name, "\$lt", value)
}
** Matches values that are less than or equal to the given object.
**
** syntax: fantom
** q.lessThanOrEqTo("score", 5)
**
** @see `https://www.mongodb.com/docs/manual/reference/operator/query/lte/`
MongoQ lessThanOrEqTo(Obj name, Obj value) {
_q.op(name, "\$lte", value)
}
// ---- Element MongoQ Operators ------------
** Matches if the field exists (or not), even if it is 'null'.
**
** syntax: fantom
** q.exists("score")
**
** @see `https://www.mongodb.com/docs/manual/reference/operator/query/exists/`
MongoQ exists(Obj name, Bool exists := true) {
_q.op(name, "\$exists", exists)
}
// ---- String MongoQ Operators -------------
** Matches string values that equal the given regular expression.
**
** syntax: fantom
** q.matchesRegex("name", "Emm?")
**
** @see `https://www.mongodb.com/docs/manual/reference/operator/query/regex/`
MongoQ matchesRegex(Obj name, Regex regex) {
_q.op(name, "\$regex", regex)
}
** Matches string values that equal (ignoring case) the given value.
** Matching is performed with regular expressions.
**
** syntax: fantom
** q.eqIgnoreCase("name", "emm?")
**
** @see `https://www.mongodb.com/docs/manual/reference/operator/query/regex/`
MongoQ eqIgnoreCase(Obj name, Str value) {
matchesRegex(name, "(?i)^${Regex.quote(value)}\$".toRegex)
}
** Matches string values that contain the given value.
** Matching is performed with regular expressions.
**
** syntax: fantom
** q.contains("name", "Em")
**
** @see `https://www.mongodb.com/docs/manual/reference/operator/query/regex/`
MongoQ contains(Obj name, Str value, Bool caseInsensitive := true) {
i := caseInsensitive ? "(?i)" : ""
return matchesRegex(name, "${i}${Regex.quote(value)}".toRegex)
}
** Matches string values that start with the given value.
** Matching is performed with regular expressions.
**
** syntax: fantom
** q.startsWith("name", "Em")
**
** @see `https://www.mongodb.com/docs/manual/reference/operator/query/regex/`
MongoQ startsWith(Obj name, Str value, Bool caseInsensitive := true) {
i := caseInsensitive ? "(?i)" : ""
return matchesRegex(name, "${i}^${Regex.quote(value)}".toRegex)
}
** Matches string values that end with the given value.
** Matching is performed with regular expressions.
**
** syntax: fantom
** q.endsWith("name", "ma")
**
** @see `https://www.mongodb.com/docs/manual/reference/operator/query/regex/`
MongoQ endsWith(Obj name, Str value, Bool caseInsensitive := true) {
i := caseInsensitive ? "(?i)" : ""
return matchesRegex(name, "${i}${Regex.quote(value)}\$".toRegex)
}
// ---- Evaluation MongoQ Operators ---------
** Matches values based on their remainder after a division (modulo operation).
**
** syntax: fantom
** q.mod("score", 3, 0)
**
** @see `https://www.mongodb.com/docs/manual/reference/operator/query/mod/`
MongoQ mod(Obj name, Int divisor, Int remainder) {
_q.op(name, "\$mod", Int[divisor, remainder]) // BSON converter is deep!
}
// ---- Logical MongoQ Operators ------------
** Selects documents that do **not** match the given following criterion.
** Example:
**
** syntax: fantom
** not.eq("score", 11)
** eq("score", 11).not
** not(q.eq("score", 11))
**
** @see `https://www.mongodb.com/docs/manual/reference/operator/query/not/`
MongoQ not(MongoQ query := this) {
if (query._val != null)
query._val = (obj["\$not"] = query._val)
else
query._not = true
return query
}
** Selects documents that pass all the query expressions in the given list.
**
** syntax: fantom
** q.and(
** q.lessThan("quantity", 20),
** q.eq("price", 10)
** )
**
** @see `https://www.mongodb.com/docs/manual/reference/operator/query/and/`
MongoQ and(MongoQ q1, MongoQ q2, MongoQ? q3 := null, MongoQ? q4 := null) {
qs := [q1._query, q2._query]
if (q3 != null) qs.add(q3._query)
if (q4 != null) qs.add(q4._query)
return _q._set("\$and", qs)
}
** Selects documents that pass any of the query expressions in the given list.
**
** syntax: fantom
** query := or(
** lessThan("quantity", 20),
** eq("price", 10)
** )
**
** @see `https://www.mongodb.com/docs/manual/reference/operator/query/or/`
MongoQ or(MongoQ q1, MongoQ q2, MongoQ? q3 := null, MongoQ? q4 := null) {
qs := [q1._query, q2._query]
if (q3 != null) qs.add(q3._query)
if (q4 != null) qs.add(q4._query)
return _q._set("\$or", qs)
}
** Selects documents that fail **all** the query expressions in the given list.
**
** syntax: fantom
** query := nor(
** lessThan("quantity", 20),
** eq("price", 10)
** )
**
** @see `https://www.mongodb.com/docs/manual/reference/operator/query/nor/`
MongoQ nor(MongoQ q1, MongoQ q2, MongoQ? q3 := null, MongoQ? q4 := null) {
qs := [q1._query, q2._query]
if (q3 != null) qs.add(q3._query)
if (q4 != null) qs.add(q4._query)
return _q._set("\$nor", qs)
}
// ---- Text Operators ----------------------
** Performs a text search on the collection.
**
** Text searching makes use of stemming and ignores language stop words.
** Quotes may be used to search for exact phrases and prefixing a word with a hyphen-minus (-) negates it.
**
** To enable text searching, ensure the Collection has a text Index else MongoDB will throw an Err.
**
** To sort by search relevance, add the following projection AND sort.
**
** syntax: fantom
** col.find(MongoQ().textSearch("quack").query ) {
** it->projection = ["_textScore": ["\$meta": "textScore"]]
** it->sort = ["_textScore": ["\$meta": "textScore"]]
** }
**
** 'options' may include the following:
**
** table:
** Name Type Desc
** ---- ---- ----
** $language Bool Determines the list of stop words for the search and the rules for the stemmer and tokenizer. See [Supported Text Search Languages]`https://docs.mongodb.com/manual/reference/text-search-languages/#text-search-languages`. Specify 'none' for simple tokenization with no stop words and no stemming. Defaults to the language of the index.
** $caseSensitive Bool Enable or disable case sensitive searching. Defaults to 'false'.
** $diacriticSensitive Int Enable or disable diacritic sensitive searching. Defaults to 'false'.
**
** @see `https://docs.mongodb.com/manual/reference/operator/query/text/`.
MongoQ textSearch(Str search, [Str:Obj?]? opts := null) {
_q := _q
_q._key = "\$text"
_q._val = (obj["\$search"] = search).setAll(opts ?: obj)
return _q
}
** Selects documents based on the return value of a javascript function. Example:
**
** syntax: fantom
** q.where("this.name == 'Judge Dredd'")
**
** Only 1 *where* function is allowed per query.
**
** @see `https://www.mongodb.com/docs/manual/reference/operator/query/where/`
MongoQ where(Str where) {
_q._set("\$where", where)
}
// ---- Other -------------------------------
Str print(Int maxWidth := 60, Str indent := " ") {
BsonIO().print(query, maxWidth, indent)
}
This dump(Int maxWidth := 60) {
echo(print(maxWidth))
return this
}
@NoDoc
override Str toStr() { print(60) }
** it->field = value
@NoDoc
override Obj? trap(Str name, Obj?[]? args := null) {
eq(name, args?.first)
}
private This _set(Obj name, Obj? value) {
_key = _nameHookFn(name)
_val = value
if (_not) {
not
_not = false
}
return this
}
** Sets an op.
**
** op("score", "\$neq", 11) --> set("score", ["\$neg", 11])
@NoDoc
This op(Obj name, Str op, Obj? value) {
_key = _nameHookFn(name)
_val = (obj[op] = _valueHookFn(value))
if (_not) {
not
_not = false
}
return this
}
// covert stuff *immediately* for instant err feedback
private |Obj ->Str | _nameHookFn
private |Obj?->Obj?| _valueHookFn
private MongoQ? _innerQ
private Str:Obj _query() { obj[_key] = _val }
private static const Func _defHookFn := |Obj? v -> Obj?| { v }.toImmutable
private MongoQ _q() {
// we can't check / throw this, 'cos we *may* be creating multiple instances for an AND or an OR filter.
//if (_innerQ != null) throw Err("Top level Mongo Query has already been set: ${toStr}")
q := _innerQ = MongoQ(_nameHookFn, _valueHookFn)
q._not = _not
_not = false
return q
}
private Str:Obj? obj() {
obj := Str:Obj?[:]
obj.ordered = true
return obj
}
}