sourceafMorphia::QueryExecutor.fan

using afMongo::Index

** Executes `Query` objects against a `Datastore`. Example:
** 
**   syntax: fantom
**   QueryExecutor(datastore, query).skip(10).limit(50).orderBy("name").findAll
** 
** Or you can use the instance returned by 'Datastore.query(...)':
** 
**   syntax: fantom
**   datastore.query(query).orderBy("name").findAll
** 
class QueryExecutor {
    internal static const Str   _textScoreFieldName := "_textScore"

    private Datastore   _datastore
    private [Str:Obj]   _query
    private Obj?        _orderBy
    private Int         _skip   := 0
    private Int?        _limit  := null
    private Bool        _textSearch
    
    
    ** Creates a 'QueryExecutor' to run the given query against the datastore.
    new make(Datastore datastore, Query query) {
        this._datastore  = datastore
        this._query      = query.toMongo(datastore)
        this._textSearch = query._textSearch
    }
    
    ** Specifies a property / field to use for ordering. 
    ** 
    ** 'name' may either an entity 'Field' annotated with '@Property' or a MongoDB property name (Str).
    ** If passing a 'Str', it may be prefixed with '-' to specify a descending order, otherwise 
    ** ordering defaults to ascending. 
    ** 
    ** Multiple calls to 'orderBy()' may be made to indicate sub-sorts.
    ** Example:
    ** 
    **   syntax: fantom
    **   QueryExecutor(...).orderBy("name").orderBy("-value").findAll
    ** 
    This orderBy(Obj name, Int sortOrder := Index.ASC) {
        fieldName := Utils.objToPropertyName(name)
        if (_orderBy is Str)
            throw ArgErr(ErrMsgs.query_canNotMixSorts(_orderBy, fieldName))
        if (_orderBy == null)
            _orderBy = map
        if (fieldName.startsWith("-"))      
            ((Str:Obj?) _orderBy)[fieldName[1..-1]] = "DESC"
        else
            ((Str:Obj?) _orderBy)[fieldName] = (sortOrder == Index.DESC) ? "DESC" : "ASC"
        return this
    }

    ** Specifies an index to use for sorting.
    This orderByIndex(Str indexName) {
        if (_orderBy != null && _orderBy isnot Str)
            throw ArgErr(ErrMsgs.query_canNotMixSorts(indexName, _orderBy))
        _orderBy = indexName
        return this
    }

    ** (Advanced)
    ** Allows you to specify your own sort document.
    This orderByDoc(Str:Obj? sortDoc) {
        if (_orderBy is Str)
            throw ArgErr(ErrMsgs.query_canNotMixSorts(_orderBy, sortDoc))
        _orderBy = sortDoc
        return this
    }

    ** When performing a text search, this orders the returned documents by search relevance.
    ** Should only be used with 'findAll()'.
    ** 
    ** Note that 'Query.textSearch()' automatically sets text score ordering. 
    This orderByTextScore(Bool order := true) {
        
        if (order == true) {
            if (_orderBy is Str)
                throw ArgErr(ErrMsgs.query_canNotMixSorts(_orderBy, ["\$meta": "textScore"]))
            if (_orderBy == null)
                _orderBy = map
            ((Str:Obj?) _orderBy)[_textScoreFieldName] = ["\$meta": "textScore"]
            _textSearch = true
        }

        if (order == false) {
            (_orderBy as Str:Obj?)?.remove(_textScoreFieldName)
            _textSearch = false         
        }

        return this
    }
    
    ** Starts the query results at a particular zero-based offset.
    ** 
    ** Only used by 'findAll()'. 
    This skip(Int skip) {
        this._skip = skip
        return this
    }

    ** Limits the fetched result set to a certain number of values. 
    ** A value of 'null' or '0' indicates no limit.
    ** 
    ** Only used by 'findAll()'. 
    This limit(Int? limit) {
        this._limit = limit
        return this
    }

    ** An (optimised) method to return one document from the query.
    ** 
    ** Throws 'MongoErr' if no documents are found and 'checked' is 'true', returns 'null' otherwise.
    ** Always throws 'MongoErr' if the query returns more than one document.
    **  
    ** @see `afMongo::Collection.findOne`
    Obj? findOne(Bool checked := true) {
        _datastore.findOne(_query, checked)
    }

    ** Returns a list of entities that match the query.
    ** 
    ** @see `afMongo::Collection.findAll`
    Obj[] findAll() {
        _datastore.findAll(_query, _orderBy, _skip, _limit, _textSearch ? [_textScoreFieldName : ["\$meta": "textScore"]] : null)
    }
    
    ** Returns the number of documents that would be returned by the query.
    ** 
    ** @see `afMongo::Collection.findCount`
    Int findCount() {
        _datastore.findCount(_query)
    }

    ** Returns a Mongo document representing the query. 
    ** May be used by `Datastore` and [Collection]`afMongo::Collection` methods such as 'findAndUpdate(...)'.  
    Str:Obj? mongoQuery() {
        _query
    }
    
    private static Str:Obj map() {
        Str:Obj[:] { ordered = true }
    }
}