** Represents a MongoDB collection.
**
** @see `https://github.com/mongodb/specifications/blob/master/source/crud/crud.rst`
const class MongoColl {
** The underlying connection manager.
const MongoConnMgr connMgr
** The name of the database.
const Str dbName
** The simple name of the collection.
const Str name
** Creates a 'Collection' with the given name under the database.
**
** Note this just instantiates the Fantom object, it does not create anything in the database.
new make(MongoConnMgr connMgr, Str name, Str? dbName := null) {
this.connMgr = connMgr
this.dbName = MongoDb.validateName(dbName ?: connMgr.mongoConnUrl.dbName)
this.name = validateName(name)
}
** Creates an 'MongoDb' instance of the associated DB.
MongoDb db() {
MongoDb(connMgr, dbName)
}
** Convenience / shorthand notation for 'findOne(["_id" : id], checked)'
@Operator
[Str:Obj?]? get(Obj? id, Bool checked := true) {
if (id == null) // quit early if ID is null
return checked ? (null ?: throw Err("findOne() returned ZERO documents from ${qname} - [_id:${id}]")) : null
return findOne(["_id" : id], checked)
}
// ---- Indexes -----------------------------
** Returns an 'MongoIdx' of the given name.
**
** Note this just instantiates the Fantom object, it does not create anything in MongoDB.
MongoIdx index(Str idxName) {
MongoIdx(connMgr, idxName, name, dbName)
}
** Returns all the indexes in this collection.
**
** @see `https://www.mongodb.com/docs/manual/reference/command/listIndexes/`
MongoCur listIndexes() {
cmd("listIndexes", name).cursor
}
** Returns all the index names in this collection.
Str[] listIndexNames() {
listIndexes.toList.map |i->Str| { i["name"] }
}
** Drops ALL indexes on the collection. *Be careful!*
**
** @see `https://www.mongodb.com/docs/manual/reference/command/dropIndexes/`
Void dropAllIndexes() {
cmd("dropIndexes", name).add("index", "*").run
}
// ---- Commands ----------------------------
** Returns 'true' if this collection exists.
Bool exists() {
MongoDb(connMgr, dbName)
.cmd("listCollections")
.add("filter", ["name":name])
.add("nameOnly", true)
.cursor
.toList.size > 0
}
** Creates a new collection explicitly.
**
** There is usually no no need to call this unless you wish explicitly set collection options.
**
** pre>
** syntax: fantom
** db.collection("name").create {
** it->capped = true
** it->size = 64 * 1024 // no of bytes
** it->max = 14 // no of docs
** }
** <pre
**
** @see `https://www.mongodb.com/docs/manual/reference/command/create/`
Void create(|MongoCmd cmd|? optsFn := null) {
cmd("create", name)
.withFn( optsFn)
.add("writeConcern", connMgr.mongoConnUrl.writeConcern)
.run
}
** Drops this collection, but only if it exists.
**
** Note that deleting all documents is MUCH quicker than dropping the Collection.
** See `deleteAll` for details.
**
** If 'force' is 'true' then no checks are made.
** This will result in an error if the collection does not exist.
**
** @see `https://www.mongodb.com/docs/manual/reference/command/drop/`
Void drop(Bool force := false) {
if (force || exists)
cmd("drop", name)
.add("writeConcern", connMgr.mongoConnUrl.writeConcern)
.run
}
** Inserts the given document.
**
** @see `https://www.mongodb.com/docs/manual/reference/command/insert/`
Void insert(Str:Obj? document) {
cmd("insert", name)
.add("documents", [document])
.add("writeConcern", connMgr.mongoConnUrl.writeConcern)
.run
}
** Inserts many documents.
**
** Default behaviour is to stop when inserting fails. See 'ordered' option for details.
**
** @see `https://www.mongodb.com/docs/manual/reference/command/insert/`
Void insertMany([Str:Obj?][] documents, |MongoCmd cmd|? optsFn := null) {
// the driver spec says I MUST raise this error!
// https://github.com/mongodb/specifications/blob/master/source/crud/crud.rst#insert-update-replace-delete-and-bulk-writes
if (documents.isEmpty)
throw ArgErr("Documents MUST not be empty.")
cmd("insert", name)
.withFn( optsFn)
.add("documents", documents)
.add("writeConcern", connMgr.mongoConnUrl.writeConcern)
.run
}
** Return one document that matches the given 'filter'.
**
** Throws an 'Err' if no documents are found and 'checked' is 'true'.
**
** Always throws 'Err' if the filter returns more than one document.
**
** @see `https://www.mongodb.com/docs/manual/reference/command/find/`
[Str:Obj?]? findOne(Str:Obj? filter, Bool checked := true) {
l := cmd("find", name)
.add("filter", filter)
.add("batchSize", 2)
.add("limit", 2)
.cursor.toList
if (l.size > 1)
throw Err("findOne() returned multiple documents from ${qname} - ${filter}")
if (l.isEmpty && checked)
throw Err("findOne() returned ZERO documents from ${qname} - ${filter}")
return l.first
}
** Returns documents that match the given filter. Many options are possible.
**
** pre>
** syntax: fantom
** find(["rick":"morty"]) {
** it->sort = ["fieldName":1]
** it->hint = "_indexName_"
** it->skip = 50
** it->limit = 100
** it->projection = ["_id":1, "name":1]
** it->batchSize = 101
** it->singleBatch = true
** it->collation = [...]
** }.toList
** <pre
**
** - @see `https://www.mongodb.com/docs/manual/reference/command/find/`
** - @see `https://www.mongodb.com/docs/manual/tutorial/query-documents/`
MongoCur find([Str:Obj?]? filter := null, |MongoCmd cmd|? optsFn := null) {
cmd("find", name)
.withFn( optsFn)
.add("filter", filter)
.cursor
}
** 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.
**
** Results are automatically ordered by search relevance.
**
** To use text searching, make sure the Collection has a text Index else MongoDB will throw an Err.
**
** col.textSearch("some text")
**
** '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'.
**
** Text searches may be mixed with regular filters. See 'MongoQ' for details.
**
** @see `https://docs.mongodb.com/manual/reference/operator/query/text/`.
MongoCur textSearch(Str search, [Str:Obj?]? opts := null) {
filter := Str:Obj?["\$text": ["\$search": search].addAll(opts ?: Str:Obj?[:])]
return cmd("find", name)
.add("filter", filter)
.add("projection", ["_textScore": ["\$meta": "textScore"]])
.add("sort", ["_textScore": ["\$meta": "textScore"]])
.cursor
}
** Runs the given 'updateCmd' against documents that match the given filter.
**
** pre>
** syntax: fantom
** update(["rick":"morty"], ["\$set":["rick":"sanchez"]]) {
** it->upsert = true
** it->multi = false // defaults to true
** it->hint = "_indexName_"
** it->collation = [...]
** }
** <pre
**
** Inspect return value for upserted IDs.
**
** *(By default, this will update multiple documents.)*
**
** @see `https://www.mongodb.com/docs/manual/reference/command/update/`
** @see `https://www.mongodb.com/docs/manual/reference/operator/update/`
Str:Obj? update(Str:Obj? filter, Str:Obj? updates, |MongoCmd cmd|? optsFn := null) {
updateCmd := cmd("q", filter)
.withFn( optsFn)
.add("u", updates)
.add("multi", true) // default to multi-doc updates
opts := updateCmd.extract("ordered writeConcern bypassDocumentValidation comment let".split)
return cmd("update", name)
.add("updates", [updateCmd.cmd])
.addAll( opts)
.add("writeConcern",connMgr.mongoConnUrl.writeConcern)
.run
}
** Finds a single document that matches the given filter, and replaces it.
** The '_id' field is NOT replaced.
**
** pre>
** syntax: fantom
** replace(["rick":"morty"], ["rick":"sanchez"]) {
** it->upsert = true
** }
** <pre
**
** @see `https://www.mongodb.com/docs/manual/reference/command/update/`
** @see `https://www.mongodb.com/docs/manual/reference/operator/update/`
Str:Obj? replace(Str:Obj? filter, Str:Obj? replacement, |MongoCmd cmd|? optsFn := null) {
updateCmd := cmd("q", filter)
.withFn( optsFn)
.add("u", replacement)
.add("multi", false)
opts := updateCmd.extract("ordered writeConcern bypassDocumentValidation comment let".split)
return cmd("update", name)
.add("updates", [updateCmd.cmd])
.addAll( opts)
.add("writeConcern",connMgr.mongoConnUrl.writeConcern)
.run
}
** Deletes all documents that match the given filter, and returns the number of documents deleted.
**
** pre>
** syntax: fantom
** delete(["rick":"morty"]) {
** it->limit = 1
** it->hint = "_indexName_"
** it->collation = [...]
** }
** <pre
**
** @see `https://www.mongodb.com/docs/manual/reference/command/delete/`
Int delete(Str:Obj? filter, |MongoCmd cmd|? optsFn := null) {
deleteCmd := cmd("q", filter)
.withFn( optsFn)
.add("limit", 0) // 0 == delete all matching documents
opts := deleteCmd.extract("comment let ordered writeConcern".split)
return cmd("delete", name)
.add("deletes", [deleteCmd.cmd])
.addAll( opts)
.add("writeConcern",connMgr.mongoConnUrl.writeConcern)
.run["n"]->toInt
}
** Deletes ALL documents in a Collection.
**
** Note this is MUCH quicker than dropping the Collection.
Int deleteAll() {
delete([:]) { it->limit=0 }
}
** Finds, updates, and returns a single document.
** Returns 'null' if no matching document was found.
**
** @see `https://www.mongodb.com/docs/manual/reference/command/findAndModify/`
[Str:Obj?]? findAndUpdate(Str:Obj? filter, Str:Obj? update, |MongoCmd cmd|? optsFn := null) {
cmd("findAndModify", name)
.withFn( optsFn)
.add("query", filter)
.add("update", update)
.add("new", true)
.add("writeConcern", connMgr.mongoConnUrl.writeConcern)
.run["value"]
}
** Finds, deletes, and returns a single document.
** Returns 'null' if no matching document was found.
**
** @see `https://www.mongodb.com/docs/manual/reference/command/findAndModify/`
[Str:Obj?]? findAndDelete(Str:Obj? filter, |MongoCmd cmd|? optsFn := null) {
cmd("findAndModify", name)
.withFn( optsFn)
.add("query", filter)
.add("remove", true)
.add("writeConcern", connMgr.mongoConnUrl.writeConcern)
.run["value"]
}
** Processes documents through an aggregation pipeline.
**
** @see
** - `http://docs.mongodb.org/manual/reference/command/aggregate/`
** - `http://docs.mongodb.org/manual/reference/aggregation/`
MongoCur aggregate([Str:Obj?][] pipeline, |MongoCmd cmd|? optsFn := null) {
cmd("aggregate", name)
.withFn( optsFn)
.add("pipeline", pipeline)
.add("cursor", Str:Obj?[:]) // MUST specify an empty cursor
.add("writeConcern",connMgr.mongoConnUrl.writeConcern)
.cursor
}
** Returns the number of documents that match the given filter.
** (Uses an 'aggregate' cmd.)
Int count([Str:Obj?]? filter := null) {
aggregate([
[
"\$match" : filter ?: Str:Obj?[:]
],
[
"\$group" : Str:Obj?[
"_id" : null,
"count" : Str:Obj?[
"\$sum" : 1
]
]
]
]).toList.first?.get("count") ?: 0
}
** Returns the number of documents in the collection.
**
** The count is based on the collection's metadata, which provides a fast but sometimes
** inaccurate count for sharded clusters.
Int size() {
aggregate([
[
"\$collStats" : [
"count" : Str:Obj?[:]
]
]
]).toList.first["count"]
}
** Returns the qualified name of this collection.
** It takes the form of:
**
** <database>.<collection>
Str qname() {
"${dbName}.${name}"
}
internal static Str validateName(Str name) {
if (name.isEmpty)
throw ArgErr("Collection name can not be empty")
if (name.any { it == '$' })
throw ArgErr("Collection name '${name}' may not contain any of the following: \$")
return name
}
@NoDoc
override Str toStr() { qname }
** **For Power Users!**
**
** Don't forget to call 'run()'!
private MongoCmd cmd(Str cmdName, Obj? cmdVal := 1) {
MongoCmd(connMgr, dbName, cmdName, cmdVal)
}
}