Refactor model proxies; add sort, limit, filter proxies

master
garrettmills 4 years ago
parent 55a25bcb32
commit 6302c31079

1
.gitignore vendored

@ -1,3 +1,4 @@
.idea/*
.env
node_modules*
set.js

@ -3,7 +3,7 @@
*/
const Focus = require('./Focus')
const FilterProxy = require('./FilterProxy')
const FilterProxy = require('../proxy/model/FilterProxy')
const { ObjectId } = require('mongodb')
/**
@ -49,7 +49,7 @@ class Filter {
/**
* End the filter and apply it to the model's proxy.
* @returns {module:flitter-orm/src/filter/FilterProxy~FilterProxy}
* @returns {module:flitter-orm/src/proxy/model/FilterProxy~FilterProxy}
*/
end() {
return new FilterProxy(this._model, this)

@ -1,89 +0,0 @@
/**
* @module flitter-orm/src/filter/FilterProxy
*/
const Proxy = require('../model/Proxy')
/**
* A proxy that applies the provided filter to the common model query methods.
* @extends module:flitter-orm/model/Proxy~Proxy
*/
class FilterProxy extends Proxy {
constructor(model, filter) {
super(model)
/**
* The filter to apply.
* @type {module:flitter-orm/src/filter/Filter~Filter}
*/
this.filter = filter
}
/**
* Apply the filter and call {@link module:flitter-orm/src/model/Model~Model#cursor}.
* @param {object} [opts = {}] - optional arguments
* @returns {mongodb/Cursor}
*/
cursor(params = {}, opts = {}) {
const filter = this.filter.clone()
filter.absorb(params)
return super.cursor(filter.write(), opts)
}
/**
* Apply the filter and call {@link module:flitter-orm/src/model/Model~Model#find}.
* @param {object} [opts = {}] - optional arguments
* @returns {Array<module:flitter-orm/src/model/Model~Model>}
*/
find(params = {}, opts = {}) {
const filter = this.filter.clone()
filter.absorb(params)
return super.find(filter.write(), opts)
}
/**
* Apply the filter and call {@link module:flitter-orm/src/model/Model~Model#findOne}.
* @param {object} [opts = {}] - optional arguments
* @returns {module:flitter-orm/src/model/Model~Model}
*/
findOne(params = {}, opts = {}) {
const filter = this.filter.clone()
filter.absorb(params)
return super.findOne(filter.write(), opts)
}
/**
* Apply the filter and call {@link module:flitter-orm/src/model/Model~Model#deleteOne}.
* @param {object} [opts = {}] - optional arguments
* @returns {*}
*/
deleteOne(params = {}, opts = {}) {
const filter = this.filter.clone()
filter.absorb(params)
return super.deleteOne(filter.write(), opts)
}
/**
* Apply the filter and call {@link module:flitter-orm/src/model/Model~Model#deleteMany}.
* @param {object} [opts = {}] - optional arguments
* @returns {*}
*/
deleteMany(params = {}, opts = {}) {
const filter = this.filter.clone()
filter.absorb(params)
return super.deleteMany(filter.write(), opts)
}
/**
* Apply the filter and call {@link module:flitter-orm/src/model/Model~Model#count}.
* @param {object} [opts = {}] - optional arguments
* @returns {number}
*/
count(params = {}, opts = {}) {
const filter = this.filter.clone()
filter.absorb(params)
return super.count(filter.write(), opts)
}
}
module.exports = exports = FilterProxy

@ -0,0 +1,71 @@
/**
* @module flitter-orm/src/model/CursorBuilder
*/
/**
* Wrapper for building up layers to apply to a cursor when it is created.
*/
class CursorBuilder {
/**
* Functions that modify the cursor, in order.
* The functions take one parameter, the cursor, and return nothing.
* @type {Array<function>}
*/
layers = []
/**
* Apply the builder's layers to a cursor.
* @param {mongodb/Cursor} cursor
* @returns {Promise<mongodb/Cursor>}
*/
async apply(cursor) {
for ( const layer of this.layers ) {
await layer(cursor)
}
return cursor
}
/**
* Add a layer that sorts the cursor.
* @param {Array<string>} sorts - array of sorts - e.g. '+field'/'-field'/'field'
*/
sort(sorts) {
const sort_obj = {}
for ( const sort of sorts ) {
if ( sort.startsWith('-') ) {
sort_obj[sort.substr(1)] = -1
} else if ( sort.startsWith('+') ) {
sort_obj[sort.substr(1)] = 1
} else {
sort_obj[sort] = 1
}
}
this.layers.push(cursor => {
cursor.sort(sort_obj)
})
}
/**
* Add a layer that limits the cursor.
* @param {number} to - max number of records
*/
limit(to) {
this.layers.push(cursor => {
cursor.limit(to)
})
}
/**
* Add a layer that applies a filter to the cursor.
* @param {module:flitter-orm/src/filter/Filter~Filter} filter
*/
filter(filter) {
this.layers.push(cursor => {
cursor.filter(filter.write())
})
}
}
module.exports = exports = CursorBuilder

@ -8,7 +8,7 @@ const Filter = require('../filter/Filter')
const { Cursor, Collection, ObjectId } = require('mongodb')
const Scope = require('./Scope')
const ResultCache = require('./ResultCache')
const WithProxy = require('./WithProxy')
/**
* The base model class. All model implementations should extend from this.
@ -121,12 +121,20 @@ class Model extends Injectable {
*/
static async find(filter, opts) {
const cursor = await this.cursor(filter, opts)
return this.from_cursor(cursor)
}
/**
* Returns an array of model instances from the specified cursor.
* @param {mongodb/cursor} cursor - the cursor
* @returns {Promise<module:flitter-orm/src/model/Model~Model>}
*/
static async from_cursor(cursor) {
const records = await cursor.toArray()
const collection = []
for ( const record of records ) {
collection.push(new this(record))
}
return collection
}
@ -163,9 +171,9 @@ class Model extends Injectable {
*/
static async findOne(filter, opts) {
const cursor = await this.cursor(filter, opts)
const records = await cursor.limit(1).toArray()
const records = await this.from_cursor(cursor.limit(1))
if ( records.length > 0 ) {
return new this(records[0])
return records[0]
}
}
@ -199,6 +207,28 @@ class Model extends Injectable {
await this.prototype.scaffold.collection(this.__name).deleteMany(filter, opts)
}
/**
* Limit the results to a specific number of records.
* @param {number} to
* @returns {module:flitter-orm/src/proxy/model/LimitProxy~LimitProxy}
*/
static limit(to) {
const LimitProxy = require('../proxy/model/LimitProxy')
return new LimitProxy(this, to)
}
/**
* Sort the results by the specified key or keys.
* @param {string} sorts... - any number of sort specifications
* @example
* Model.sort('+last_name', '+first_name', '-create_date')
* @returns {module:flitter-orm/src/proxy/model/SortProxy~SortProxy}
*/
static sort(...sorts) {
const SortProxy = require('../proxy/model/SortProxy')
return new SortProxy(this, sorts)
}
/**
* Count the number of instances of this model with the specified filters.
*
@ -219,30 +249,19 @@ class Model extends Injectable {
* pre-loaded with any scopes.
* @returns {module:flitter-orm/src/filter/Filter~Filter}
*/
static async filter() {
let filter = new Filter(this)
static async filter(ref = false) {
if ( !ref ) ref = this
let filter = new Filter(ref)
for ( const scope of this.scopes ) {
filter = await scope.filter(filter)
}
return filter
}
/**
* Get a WithProxy to pre-load the specified relationships
* when the models are queried.
* @example
* User.with('settings').find({active: true})
* @param {...string} relationships - any number of relationship names as parameters
* @returns {module:flitter-orm/src/model/WithProxy~WithProxy}
*/
static with(...relationships) {
return new WithProxy(this, relationships)
}
/**
* Create a new instance of this model.
* @param {object} [data = {}] - data to preload the model with
* @param {module:flitter-orm/src/model/Model~Model|boolean} [embedded_parent = false] - if specified, sets the embedded parent of this model for saving
*/
constructor(data = {}, embedded_parent = false) {
super()
@ -335,6 +354,15 @@ class Model extends Injectable {
return this.scaffold.collection(this.constructor.__name)
}
/**
* Get the MongoDB collection instance for this model.
* @returns {Collection}
* @private
*/
static __collection() {
return this.prototype.scaffold.collection(this.__name)
}
/**
* Shallow copy the values from the specified object to this model.
* @param {object} data

@ -1,74 +0,0 @@
/**
* @module flitter-orm/src/model/Proxy
*/
/**
* A class for creating proxies in front of a model with
* relevant information.
* @class
*/
class Proxy {
constructor(model) {
/**
* The model to proxy.
* @type {module:flitter-orm/src/model/Model~Model}
*/
this.model = model
}
/**
* Apply the filter and call {@link module:flitter-orm/src/model/Model~Model#cursor}.
* @param {object} [opts = {}] - optional arguments
* @returns {mongodb/Cursor}
*/
cursor(opts = {}) {
return this.model.cursor({}, opts)
}
/**
* Apply the filter and call {@link module:flitter-orm/src/model/Model~Model#find}.
* @param {object} [opts = {}] - optional arguments
* @returns {Array<module:flitter-orm/src/model/Model~Model>}
*/
find(filter = {}, opts = {}) {
return this.model.find(filter, opts)
}
/**
* Apply the filter and call {@link module:flitter-orm/src/model/Model~Model#findOne}.
* @param {object} [opts = {}] - optional arguments
* @returns {module:flitter-orm/src/model/Model~Model}
*/
findOne(filter = {}, opts = {}) {
return this.model.findOne(filter, opts)
}
/**
* Apply the filter and call {@link module:flitter-orm/src/model/Model~Model#deleteOne}.
* @param {object} [opts = {}] - optional arguments
* @returns {*}
*/
deleteOne(filter = {}, opts = {}) {
return this.model.deleteOne(filter, opts)
}
/**
* Apply the filter and call {@link module:flitter-orm/src/model/Model~Model#deleteMany}.
* @param {object} [opts = {}] - optional arguments
* @returns {*}
*/
deleteMany(filter = {}, opts = {}) {
return this.model.deleteMany(filter, opts)
}
/**
* Apply the filter and call {@link module:flitter-orm/src/model/Model~Model#count}.
* @param {object} [opts = {}] - optional arguments
* @returns {number}
*/
count(filter = {}, opts = {}) {
return this.model.count(filter, opts)
}
}
module.exports = exports = Proxy

@ -1,100 +0,0 @@
/**
* @module flitter-orm/src/model/WithProxy
*/
const Proxy = require('./Proxy')
/**
* Proxy that pre-loads model relationships on query.
* @extends module:flitter-orm/src/model/Proxy~Proxy
*/
class WithProxy extends Proxy {
/**
* List of relationship names to preload.
* @private
* @type {Array<string>}
*/
#relationships = []
/**
* Instantiate the proxy.
* @param {module:flitter-orm/src/model/Model~Model} model - static CLASS of the model we're proxying
* @param {Array<string>} [with_relationships = []] - relationship names to preload
*/
constructor(model, with_relationships = []) {
super(model)
this.#relationships = with_relationships
}
/**
* Find multiple records matching the provided filter. Relationships will be pre-loaded.
* @param {object} [filter = {}] - some filter parameters
* @param {object} [opts = {}] - optional MongoDB arguments
* @returns {Promise<Array<module:flitter-orm/src/model/Model~Model>>}
*/
async find(filter = {}, opts = {}) {
const results = await super.find(filter, opts)
const promises = results.map(result => this._load_relationships_for(result))
await Promise.all(promises)
return results
}
/**
* Find a single record matching the provided filter. Relationships will be pre-loaded.
* @param {object} [filter = {}] - some filter parameters
* @param {object} [opts = {}] - optional MongoDB arguments
* @returns {Promise<module:flitter-orm/src/model/Model~Model>}
*/
async findOne(filter = {}, opts = {}) {
const result = await super.findOne(filter, opts)
await this._load_relationships_for(result)
return result
}
/**
* Pre-load the relationships configured in this proxy for the
* provided model instance.
* @param {module:flitter-orm/src/model/Model~Model} model
* @returns {Promise<void>}
* @private
*/
async _load_relationships_for(model) {
for ( const relation of this.#relationships ) {
if ( this._is_getter(model, relation) ) {
await model[relation]
} else if ( typeof model[relation] === 'function' ) {
await model[relation]()
}
}
}
/**
* Checks if the specified property name on the object is a virtual getter.
* @param {object|function} object - the object to check
* @param {string} property - the name of the property
* @returns {boolean} - true if object[property] is a getter
* @private
*/
_is_getter(object, property) {
return !!this._getPropertyDescriptor(object, property).get
}
/**
* Get the ancestral property descriptor for the specified object.
* This is like 'getOwnPropertyDescriptor', but it searches the entire
* prototype chain.
* @param {object|function} object - the object to search
* @param {string} property - the name of the property
* @returns {PropertyDescriptor}
* @private
*/
_getPropertyDescriptor(object, property) {
let descending
do {
descending = Object.getOwnPropertyDescriptor(object, property)
} while ( !descending && (object = Object.getPrototypeOf(object)) )
return descending
}
}
module.exports = exports = WithProxy

@ -0,0 +1,23 @@
/**
* @module flitter-orm/src/proxy/model/FilterProxy
*/
const ModelProxy = require('./ModelProxy')
/**
* Proxy that applies a filter to the reference's result set.
* @extends module:flitter-orm/src/proxy/model/ModelProxy
*/
class FilterProxy extends ModelProxy {
/**
* Instantiate the proxy.
* @param {module:flitter-orm/src/model/Model~Model|module:flitter-orm/src/proxy/model/ModelProxy~ModelProxy} model - proxy ref
* @param {module:flitter-orm/src/filter/Filter~Filter} filter - the filter to apply
*/
constructor(model, filter) {
super(model)
this.builder.filter(filter)
}
}
module.exports = exports = FilterProxy

@ -0,0 +1,23 @@
/**
* @module flitter-orm/src/proxy/model/LimitProxy
*/
const ModelProxy = require('./ModelProxy')
/**
* Proxy that limits the number of records in the reference's result set.
* @extends module:flitter-orm/src/proxy/model/ModelProxy~ModelProxy
*/
class LimitProxy extends ModelProxy {
/**
* Instantiate the proxy
* @param {module:flitter-orm/src/model/Model~Model|module:flitter-orm/src/proxy/model/ModelProxy~ModelProxy} model - proxy ref
* @param {number} limit - the number to limit to
*/
constructor(model, limit) {
super(model)
this.builder.limit(limit)
}
}
module.exports = exports = LimitProxy

@ -0,0 +1,155 @@
/**
* @module flitter-orm/src/proxy/model/ModelProxy
*/
const CursorBuilder = require('../../model/CursorBuilder')
const { ObjectId } = require('mongodb')
/**
* A proxy class for adding restrictive layers to a model.
*/
class ModelProxy {
/**
* Instantiates the proxy.
* @param {module:flitter-orm/src/model/Model~Model|module:flitter-orm/src/proxy/model/ModelProxy~ModelProxy} reference - the parent reference
*/
constructor(reference) {
/**
* The cursor builder applied by this proxy.
* @type {module:flitter-orm/src/model/CursorBuilder~CursorBuilder}
*/
this.builder = new CursorBuilder()
/**
* The parent reference of this proxy.
* @type {module:flitter-orm/src/model/Model~Model|module:flitter-orm/src/proxy/model/ModelProxy~ModelProxy}
*/
this.reference = reference
}
/**
* Get the collection instance for the proxy reference.
* @returns {Promise<mongodb/Collection>}
* @private
*/
async __collection() {
return this.reference.__collection()
}
/**
* Get a cursor for the reference, and apply the proxy's builder.
* @param {object} [filter = {}] - mongodb filter options
* @param {object} [opts = {}] - optional mongodb params
* @returns {Promise<*>}
*/
async cursor(filter = {}, opts = {}) {
const cursor = await this.reference.cursor(filter, opts)
return await this.builder.apply(cursor)
}
/**
* Find a set of records subject to this proxy's restrictions.
* @param {object} [filter = {}] - mongodb filters to apply
* @param {object} [opts = {}] - optional mongodb params
* @returns {Promise<Array<module:flitter-orm/src/model/Model~Model>>}
*/
async find(filter = {}, opts = {}) {
const cursor = await this.cursor(filter, opts)
return this.reference.from_cursor(cursor)
}
/**
* Find a single record subject to this proxy's restrictions.
* @param {object} [filter = {}] - mongodb filters to apply
* @param {object} [opts = {}] - optional mongodb params
* @returns {Promise<module:flitter-orm/src/model/Model~Model>}
*/
async findOne(filter, opts) {
const cursor = await this.cursor(filter, opts)
return (await this.reference.from_cursor(cursor.limit(1)))[0]
}
/**
* Delete the set of records subject to this proxy's restrictions.
* @param {object} [filter = {}] - mongodb filters to apply
* @param {object} [opts = {}] - optional mongodb params
* @returns {Promise<void>}
*/
async deleteMany(filter, opts) {
const cursor = await this.cursor(filter, opts)
const ids = (await cursor.toArray()).map(x => x._id)
await this._bulk_delete(ids)
}
/**
* Delete a single record subject to this proxy's restrictions.
* @param {object} [filter = {}] - mongodb filters to apply
* @param {object} [opts = {}] - optional mongodb params
* @returns {Promise<module:flitter-orm/src/model/Model~Model>} - the deleted model
*/
async deleteOne(filter, opts) {
const cursor = await this.cursor(filter, opts)
const rec = (await this.reference.from_cursor(cursor))[0]
return rec.delete()
}
/**
* Get an array of model instances from the provided cursor.
* @param {mongodb/cursor} cursor
* @returns {Promise<Array<module:flitter-orm/src/model/Model~Model>>}
*/
async from_cursor(cursor) {
return this.reference.from_cursor(cursor)
}
/**
* Delete all model instances matched by the provided IDs
* @param {Array<string|mongodb/ObjectId>} ids
* @returns {Promise<void>}
* @private
*/
async _bulk_delete(ids) {
ids = ids.map(x => {
if ( typeof x === 'string' ) return ObjectId(x)
return x
})
const coll = await this.__collection()
await coll.deleteMany({_id: {$in: ids}})
}
// ----------- Helpers for building proxy chains --------------- //
/**
* Limit the results to a specified number of records.
* @param {number} to
* @returns {module:flitter-orm/src/proxy/model/LimitProxy~LimitProxy}
*/
limit(to) {
const LimitProxy = require('./LimitProxy')
return new LimitProxy(this, to)
}
/**
* Sort the result set by the provided field(s).
* @param {string} sorts... - variable number of fields to sort
* @example
* Model.sort('+first_name', '+last_name', '-create_date')
* @returns {module:flitter-orm/src/proxy/model/SortProxy~SortProxy}
*/
sort(...sorts) {
const SortProxy = require('./SortProxy')
return new SortProxy(this, sorts)
}
/**
* Get a filter object whose reference is this proxy.
* @param {module:flitter-orm/src/model/Model|module:flitter-orm/src/proxy/model/ModelProxy} ref
* @returns {Promise<module:flitter-orm/src/filter/Filter~Filter>}
*/
async filter(ref = false) {
if ( !ref ) ref = this
return this.reference.filter(ref)
}
}
module.exports = exports = ModelProxy

@ -0,0 +1,23 @@
/**
* @module flitter-orm/src/proxy/model/SortProxy
*/
const ModelProxy = require('./ModelProxy')
/**
* Proxy that applies a number of sorts to the cursor.
* @extends module:flitter-orm/src/proxy/model/ModelProxy~ModelProxy
*/
class SortProxy extends ModelProxy {
/**
* Instantiate the proxy.
* @param {module:flitter-orm/src/model/Model~Model|module:flitter-orm/src/proxy/model/ModelProxy~ModelProxy} model - proxy ref
* @param {Array<string>} sorts - array of sorts ('-field', '+field', or 'field')
*/
constructor(model, sorts) {
super(model)
this.builder.sort(sorts)
}
}
module.exports = exports = SortProxy
Loading…
Cancel
Save