Last active
January 15, 2018 10:48
-
-
Save philcockfield/5033395 to your computer and use it in GitHub Desktop.
Meteor - Model (Logical Document Wrappers).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
do -> | |
core = APP.ns 'core' | |
Model = core.Model | |
singletonManagers = {} | |
### | |
Base class for models that represent Mongo documents. | |
This provides a way of wrapping model logic around | |
a document instance. | |
### | |
core.DocumentModel = class DocumentModel extends Model | |
### | |
Constructor. | |
@param doc: The document instance being wrapped. | |
@param schema: The schema Type (or an instance) that defines the model's properties. | |
@param collection: The Mongo collection the document resides within. | |
### | |
constructor: (doc, schema, collection) -> | |
@_collection = collection | |
super doc, schema | |
@id = @_doc._id | |
### | |
Disposes of the object. | |
### | |
dispose: -> | |
super | |
@_session?.dispose() | |
delete @_session | |
### | |
Retrieves the scoped session for the model. | |
This can be used client-side for storing view state information. | |
### | |
session: -> | |
return if @isDisposed | |
@_session = DocumentModel.session(@) unless @_session? | |
@_session | |
### | |
The default selector to use for updates and deletes | |
### | |
defaultSelector: -> @id | |
### | |
Assumes an unsaved document has been created. | |
Inserts the document, and inserts the db into the document | |
### | |
insertNew: -> | |
# Setup initial conditions. | |
throw new Error('Already exists.') if @id? | |
# Ensure all default values from the schema are on the document. | |
@setDefaultValues() | |
doc = @_doc | |
# Insert into collection. | |
newId = @_collection.insert(doc) | |
doc._id = newId | |
@id = newId | |
# Finish up. | |
@ | |
### | |
Updates the document in the DB. | |
@param updates: The change instructions, eg $set:{ foo:123 } | |
@param options: Optional Mongo update options. | |
### | |
update: (updates, options) -> | |
@_collection.update( @defaultSelector(), updates, options ) | |
### | |
Updates the specified fields. | |
@param fields: The schema definitions of the fields to save. | |
### | |
updateFields: (fields...) -> | |
# Setup initial conditions. | |
fields = fields.map (f) -> | |
if Object.isFunction(f) then f.definition else f | |
fields = fields.flatten() | |
# Set the 'updatedAt' timestamp if required. | |
do => | |
updatedAt = @fields.updatedAt | |
if updatedAt?.type?.name is 'Date' | |
alreadyExists = fields.find (item) -> item.key is updatedAt.key | |
unless alreadyExists | |
@updatedAt +(new Date()) | |
fields.add( updatedAt ) | |
# Build the change-set. | |
change = {} | |
for item in fields | |
prop = @[item.key] | |
if item.modelRef? | |
# A referenced model. Pass the sub-document to be saved. | |
value = prop._doc | |
else | |
# A property-func, read the value. | |
value = prop.apply(@) | |
# Cast it to an integer if it's a date - see http://stackoverflow.com/questions/2831345/is-there-a-way-to-check-if-a-variable-is-a-date-in-javascript | |
value = +(value) if Object.prototype.toString.call(value) == "[object Date]" # | |
# Store the value on the change-set | |
change[item.field] = value | |
# Save. | |
@update $set:change | |
### | |
Deletes the model. | |
### | |
delete: -> @_collection.remove( @defaultSelector() ) | |
### | |
Re-queries the document from the collection. | |
### | |
refresh: -> | |
return unless @_collection? and @_schema? | |
doc = @_collection.findOne( @id ) | |
@_init( doc ) if doc? | |
@ | |
# Class Properties --------------------------------------------------------- | |
DocumentModel.isDocumentModelType = true # Flag used to identify the type. | |
### | |
Gets the scoped-session singleton for the given model instance. | |
@param instance: The [DocumentModel] instance to retrieve the session for. | |
### | |
DocumentModel.session = (instance) -> | |
return unless instance?.id? | |
core.ScopedSession.singleton( "#{ Model.typeName(instance) }:#{instance.id}" ) | |
### | |
Retrieves a singleton instance of a model. | |
@param id: The model/document ID. | |
@param fnFactory: The factory method (or Type) to create the model with if it does not already exist. | |
### | |
DocumentModel.singleton = (id, fnFactory) -> | |
# Setup initial conditions. | |
return unless id? | |
doc = id if id._id | |
id = doc._id if doc? | |
# Create the instance if necessary. | |
unless instances[id]? | |
if fnFactory?.isDocumentModelType and doc? | |
# Create from Type. | |
instances[id] = new fnFactory(doc) | |
else if Object.isFunction(fnFactory) | |
# Create from factory function. | |
instances[id] = fnFactory?(id) | |
# Retrieve the model. | |
model = instances[id] | |
manageSingletons(model?._collection) # Ensure singletons are removed. | |
model | |
DocumentModel.instances = instances = {} | |
manageSingletons = do -> | |
collections = {} | |
(col) -> | |
return unless col? | |
return if collections[col._name]? | |
collections[col._name] = col | |
col.find().observe | |
removed: (oldDoc) -> | |
# console.log 'single removed', oldDoc | |
id = oldDoc._id | |
model = instances[id] | |
model?.dispose() | |
delete instances[id] | |
### | |
Writes a debug log of the model instances. | |
### | |
instances.write = -> | |
items = [] | |
add = (instance) -> items.add(instance) unless Object.isFunction(value) | |
add(value) for key, value of instances | |
console.log "core.DocumentModel.instances (singletons): #{items.length}" | |
for instance in items | |
console.log ' > ', instance | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
### | |
Defines a user of the system. | |
### | |
UserSchema = class ns.UserSchema extends APP.core.Schema | |
constructor: (fields...) -> super fields, | |
services: undefined # Services field that gets created by the Meteor framework | |
name: # Name object. | |
field: 'profile.name' | |
modelRef: -> ns.Name | |
email: | |
field: 'profile.email' | |
roleRefs: # Collection of ID references to roles the user is within | |
field: 'profile.roleRefs' | |
### | |
Represents a user of the system. | |
### | |
User = class ns.User extends APP.core.DocumentModel | |
constructor: (doc) -> | |
super doc, UserSchema, meteor.users |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
do -> | |
### | |
Represents a field on a model. | |
### | |
class APP.core.FieldDefinition | |
### | |
Constructor. | |
@param key: The name of the field. | |
@param definition: The field as defined in the schema. | |
### | |
constructor: (@key, definition) -> | |
def = definition | |
@field = def?.field ? @key | |
# Model-ref. | |
if def?.modelRef? | |
@modelRef = def.modelRef if Object.isFunction(def.modelRef) | |
# Has-one ref. | |
hasOne = def?.hasOne | |
if hasOne? | |
for refKey in [ 'key', 'modelRef' ] | |
throw new Error("HasOne ref for '#{@key}' does not have a #{refKey}.") unless hasOne[refKey]? | |
@hasOne = | |
key: hasOne.key | |
modelRef: hasOne.modelRef | |
# type: hasOne.type | |
# collection: hasOne.collection | |
# Type. | |
@type = def.type if def?.type? | |
# Default value. | |
unless @modelRef? | |
if Object.isObject(def) | |
@default = def.default | |
else | |
@default = def | |
@default = @default() if Object.isFunction(@default) | |
### | |
Determines whether there is a default value. | |
### | |
hasDefault: -> @default isnt undefined | |
copyTo: (target) -> | |
target.definition = @ | |
for key, value of @ | |
target[key] = value unless Object.isFunction(value) | |
### | |
Reads the value of the field from the given document. | |
@param doc: The document object to read from. | |
### | |
read: (doc) -> | |
# Setup initial conditions. | |
mapTo = @field | |
unless mapTo.has('.') | |
# Shallow read. | |
value = doc[mapTo] | |
else | |
# Deep read. | |
parts = mapTo.split('.') | |
key = parts.last() | |
parts.removeAt(parts.length - 1) | |
value = APP.ns.get(doc, parts)[key] | |
# Process the raw value. | |
if value is undefined | |
value = @default | |
else if @type? | |
# Type conversions. | |
if @type is Date and value isnt @default and not Object.isDate(value) | |
value = new Date(value) | |
# Finish up. | |
value | |
### | |
Writes the field value to the given document. | |
@param doc: The document object to write to. | |
@param field: The schema field definition. | |
@param value: The value to write. | |
### | |
write: (doc, value) -> | |
value = @default if value is undefined | |
target = docTarget(@field, doc) | |
target.obj[ target.key ] = value | |
### | |
Deletes the field from the document. | |
### | |
delete: (doc) -> | |
target = docTarget(@field, doc) | |
delete target.obj[target.key] | |
docTarget = (field, doc) -> | |
unless field.has('.') | |
# Shallow write. | |
return { key:field, obj: doc } | |
else | |
# Deep write. | |
parts = field.split('.') | |
target = doc | |
for part, i in parts | |
if i is parts.length - 1 | |
# write(target, part) # Write value. | |
return { key: part, obj: target } | |
else | |
target[part] ?= {} | |
target = target[part] | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
do -> | |
core = APP.ns 'core' | |
instanceCount = 0 | |
### | |
Base class models. | |
This provides a way of wrapping model logic around | |
a document instance. | |
### | |
core.Model = class Model | |
### | |
Constructor. | |
@param doc: The document instance being wrapped. | |
@param schema: The schema Type (or an instance) that defines the model's properties. | |
### | |
constructor: (doc, schema) -> | |
instanceCount += 1 | |
@_instance = instanceCount | |
@_schema = schema | |
@_init( doc ) | |
_init: (doc) -> | |
@_doc = doc ? {} | |
unless @fields | |
# First time initialization. | |
if @_schema? | |
applySchema( @ ) if @_schema? | |
applyModelRefs( @, overwrite:false ) | |
else | |
# This is a refresh of the document. | |
applyModelRefs( @, overwrite:true ) | |
### | |
Disposes of the object. | |
### | |
dispose: -> @isDisposed = true | |
### | |
Retrieves the schema instance that defines the model. | |
### | |
schema: -> | |
# Setup initial conditions. | |
return unless @_schema | |
# Ensure the value is a Schema instance. | |
if Object.isFunction( @_schema ) | |
throw new Error('Not a schema Type.') unless @_schema.isSchema is yes | |
@_schema = @_schema.singleton() | |
else | |
throw new Error('Not a schema instance.') unless (@_schema instanceof core.Schema) | |
# Finish up. | |
@_schema | |
### | |
Merges into the models document default values from the | |
schema for values that are not already present. | |
### | |
setDefaultValues: -> | |
schema = @schema() | |
if schema? | |
Object.merge @_doc, schema.createWithDefaults(), true, false | |
@ | |
# Class Properties --------------------------------------------------------- | |
Model.isModelType = true # Flag used to identify the type. | |
### | |
Gets the type name of the given model instance | |
@param instance: The instance of the model to examine. | |
### | |
Model.typeName = (instance) -> instance?.__proto__.constructor.name | |
# Private ------------------------------------------------------------ | |
assign = (model, key, value, options = {}) -> | |
unless options.overwrite is true | |
throw new Error("The field '#{key}' already exists.") if model[key] isnt undefined | |
model[key] = value | |
applySchema = (model) -> | |
schema = model.schema() | |
# Store a reference to the fields. | |
model.fields ?= schema.fields | |
# Apply fields. | |
for key, value of schema.fields | |
unless value.modelRef? | |
# Assign a read/write property-function. | |
assign( model, key, fnField(value, model) ) | |
if value.hasOne? | |
assign model, value.hasOne.key, fnHasOne(value, model) | |
applyModelRefs = (model, options = {}) -> | |
schema = model.schema() | |
for key, value of schema.fields | |
if value.modelRef? | |
# Assign an instance of the referenced model. | |
# NB: Assumes the first parameter of the constructor is the document. | |
doc = APP.ns.get( model._doc, value.field ) | |
instance = new value.modelRef(doc) | |
# Check if the function returns the Type (rather than the instance). | |
if Object.isFunction(instance) and instance.isModelType | |
instance = new instance(doc) | |
# Store the model-ref parent details. | |
instance._parent ?= # Don't overrite an existing value. | |
model: model | |
field: value | |
# Assign the property-function. | |
assign model, key, instance, options | |
fnField = (field, model) -> | |
fn = (value, options) -> | |
# Setup initial conditions. | |
doc = model._doc | |
# Write value. | |
if value isnt undefined | |
value = beforeWriteFilter(@, field, value, options) | |
field.write(doc, value) | |
afterWriteFilter(@, field, value, options) | |
# Persist to mongo DB (if requsted). | |
if options?.save is true | |
if model.updateFields? | |
# This is a [DocumentModel] that can be directly updated. | |
model.updateFields?(field) | |
else | |
parent = model._parent | |
if parent?.model.updateFields? | |
# This is a sub-document model. Update on the parent. | |
parent.model.updateFields?( parent.field ) | |
# Read value. | |
field.read(doc) | |
# Finish up. | |
copyCommonFunctionProperties(fn, field, model) | |
fn | |
fnDelete = (field) -> | |
-> | |
field.delete(@model._doc) | |
fnHasOne = (field, model) -> | |
fn = (value, options) -> | |
read = => | |
# Setup initial conditions. | |
hasOne = field.hasOne | |
privateKey = '_' + hasOne.key | |
# Look up the ID of the referenced model. | |
idRef = @[field.key]() | |
return unless idRef? | |
# Check whether the model has already been cached. | |
isCached = @[privateKey]? and @[privateKey].id is idRef | |
return @[privateKey] if isCached | |
# Construct the model from the factory. | |
@[privateKey] = hasOne.modelRef(idRef) | |
write = => | |
# Store the ID of the written object in the ref field. | |
value = beforeWriteFilter(@, field, value.id, options) | |
options ?= {} | |
options.ignoreBeforeWrite = true | |
@[field.key] value, options | |
# Read and write. | |
write() if value isnt undefined | |
read() | |
# Finish up. | |
copyCommonFunctionProperties(fn, field, model) | |
fn | |
copyCommonFunctionProperties = (fn, field, model) -> | |
field.copyTo(fn) | |
fn.model = model | |
fn.delete = fnDelete(field) | |
fn | |
beforeWriteFilter = (model, field, value, options) -> | |
return value if options?.ignoreBeforeWrite is true | |
writeFilter(model, field, value, options, 'beforeWrite') | |
afterWriteFilter = (model, field, value, options) -> | |
return value if options?.ignoreAfterWrite is true | |
writeFilter(model, field, value, options, 'afterWrite') | |
writeFilter = (model, field, value, options, filterKey) -> | |
fnFilter = model[field.key][filterKey] | |
return value unless Object.isFunction( fnFilter ) | |
value = fnFilter(value, options) | |
value | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
do -> | |
core = APP.ns 'core' | |
### | |
A collection of models that correspond to an array of references. | |
### | |
class core.ModelRefsCollection | |
### | |
@param parent: The parent model. | |
@param refsKey: The key of the property-function that contains the array. | |
@param fnFactory(id): Factory method that creates a new model from the given id. | |
### | |
constructor: (@parent, @refsKey, @fnFactory) -> | |
throw new Error("[#{@refsKey}] not found") unless @parent[@refsKey] | |
### | |
The total number of parents of the particle. | |
### | |
count: -> @refs().length | |
### | |
Determines whether the collection is empty. | |
### | |
isEmpty: -> @count() is 0 | |
### | |
Gets the collection of refs | |
### | |
refs: -> @parent[@refsKey]() ? [] | |
### | |
Retrieves the collection of models. | |
### | |
toModels: -> | |
return [] unless @fnFactory | |
result = @refs().map (id) => @fnFactory(id) | |
result.compact() | |
### | |
Determines whether the given model exists within the collection. | |
@param model: The model (or ID) to look for. | |
### | |
contains: (model) -> | |
return false unless model? | |
id = model.id ? model | |
if id | |
@refs().indexOf(id) isnt -1 | |
else | |
false | |
### | |
Adds a new model to the collection. | |
@param model: The model(s), or the model(s) ID's, to add. | |
@param options | |
- save: Flag indicating if the parent refs array should | |
be updated in the DB (default:true) | |
### | |
add: (model, options = {}) -> | |
# Setup initial conditions. | |
model = [model] unless Object.isArray(model) | |
add = (item) => | |
if Object.isObject(item) | |
item.insertNew() unless item.id | |
id = item.id ? item | |
refs = @refs() | |
return if refs.find (ref) -> ref is id # Don't add duplicates. | |
# Add the reference. | |
refs.add(id) | |
@_writeRefs(refs, save:false) | |
add(m) for m in model.compact() | |
# Save if required. | |
options.save ?= true | |
@_saveParentRefs() if options.save is true | |
# Finish up. | |
@ | |
### | |
Removes the given object. | |
@param model: The model(s), or the model(s) ID's, to remove. | |
@param options | |
- save: Flag indicating if the parent refs array should | |
be updated in the DB (default:true) | |
### | |
remove: (model, options = {}) -> | |
# Setup initial conditions. | |
model = [model] unless Object.isArray(model) | |
remove = (item) => | |
id = item.id ? item | |
refs = @refs() | |
beforeCount = refs.length | |
# Attempt to remove the reference. | |
refs.remove (item) -> item is id | |
if refs.length isnt beforeCount | |
# An item was removed. | |
@_writeRefs(refs, save:false) | |
remove(m) for m in model.compact() | |
# Save if required. | |
options.save ?= true | |
@_saveParentRefs() if options.save is true | |
# Finish up. | |
@ | |
### | |
Removes all parent references. | |
@param options | |
- save: Flag indicating if the particle models should | |
be updated in the DB (default:true) | |
### | |
clear: (options = {}) -> | |
options.save ?= true | |
@_writeRefs([], save:options.save) | |
@ | |
_writeRefs: (value, options) -> | |
@parent[@refsKey](value, options) | |
_saveParentRefs: -> @parent.updateFields( @parent.fields[@refsKey] ) | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
do -> | |
### | |
Base class for model Schema's | |
### | |
Schema = class APP.core.Schema | |
### | |
Constructor. | |
@param fields: Object(s) containing a set of field definitions | |
passed to the [define] method. | |
### | |
constructor: (fields...) -> | |
# Setup initial conditions. | |
@fields = {} | |
# Reverse the fields so that child overrides replace parent definitions. | |
fields.reverse() | |
# Setup the field definitions. | |
define = (field) => | |
return unless field? | |
for key, value of field | |
@fields[key] = new APP.core.FieldDefinition(key, value) | |
define( item ) for item in fields.flatten() | |
### | |
Creates a new document object populated with the | |
default values of the schema. | |
@param schema: The schema to generate the document from. | |
### | |
createWithDefaults: -> | |
doc = {} | |
for key, field of @fields | |
if field.hasDefault() | |
value = field.default | |
field.write(doc, value) | |
doc | |
# Schema Class Methods ------------------------------------------------------------ | |
Schema.isSchema = true # Flag used to identify the type. | |
Schema.isInitialized = false # Flag indicating if the schema has been initialized. | |
### | |
Initializes the schema, copying the field definitions | |
as class properties. | |
### | |
Schema.init = -> | |
return @ if @isInitialized is true | |
instance = @singleton() | |
@fields ?= instance.fields | |
Object.merge @, instance.fields | |
# Finish up. | |
@isInitialized = true | |
@ | |
### | |
Retrieves the singleton instance of the schema. | |
### | |
Schema.singleton = -> | |
return @_instance if @_instance? | |
@_instance = new @() | |
@_instance | |
### | |
The common date fields applied to objects. | |
### | |
Schema.dateFields = | |
createdAt: # The date the model was created. | |
default: -> +(new Date()) | |
type: Date | |
updatedAt: # The date the model was last updated in the DB. | |
default: undefined | |
type: Date | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment