Skip to content

Instantly share code, notes, and snippets.

@abe33
Last active August 29, 2015 14:06
Show Gist options
  • Save abe33/eb3ea850da941498e155 to your computer and use it in GitHub Desktop.
Save abe33/eb3ea850da941498e155 to your computer and use it in GitHub Desktop.
ActiveRecord-like mixin
# Creates a collection class for the given model. This class
# will be decorated with the scopes defined on the model class.
build_collection_class = (model) ->
# The Collection class behaves mostly like an array except that
# every methods that should return an array return a collection
# instead.
class Collection
@model: model
# We can't use `new Collection` because Collection's instances
# are not proxy. Use `Collection.create` instead.
@create: (content) ->
collection = new @(content)
new Proxy collection, {
get: (target, name) ->
if typeof target[name] is 'function'
target[name].bind(target)
else
if /-?\d+/.test(name)
# With this we can call `collection[index]` and access the content
# of the collection array.
index = parseInt(name)
index = target.length + index if index < 0
target.array[index]
else
target[name]
}
@delegate_array_methods: (methods...) ->
methods.forEach (method) =>
@::[method] = -> @array[method](arguments...)
@delegate_array_returning_methods: (methods...) ->
methods.forEach (method) =>
@::[method] = -> @constructor.create(@array[method](arguments...))
@delegate_array_methods 'push', 'pop', 'shift', 'unshift', 'length', 'forEach', 'some', 'every', 'indexOf'
@delegate_array_returning_methods 'concat', 'splice', 'slice', 'filter'
constructor: (@array=[]) ->
first: -> @array[0]
last: -> @array[@length - 1]
map: (block) ->
results = if typeof block is 'string'
@array.map (el) -> el[block]
else
@array.map(block)
new @constructor(results)
distinct: (field) ->
values = []
@forEach (instance) ->
values.push(instance[field]) unless instance[field] in values
values
where: (conditions={}) ->
@filter (model) => @match_conditions(model, conditions)
match_conditions: (model, conditions) ->
res = true
for k,v of conditions
if typeof v is 'function'
res &&= v(model[k])
else
res &&= model[k] is v
res
class Model
@delegate_collection_methods: (methods...) ->
methods.forEach (method) => @[method] = -> @instances[method](arguments...)
@delegate_collection_methods 'first', 'last', 'where', 'distinct'
### Public ###
# Initializes the instances collection on the model's class.
@initialize_collection: ->
return if @initialized
@Collection = build_collection_class(this)
@instances = @Collection.create()
@nextId = 1
@initialized = true
@create_collection: (content=[]) ->
@initialize_collection()
@Collection.create(content)
@register: (instance) ->
@initialize_collection()
instance.id = @nextId++
@instances.push(instance)
@unregister: (instance) ->
@instances.splice(@instances.indexOf(instance), 1) if instance in @instances
### Scopes ###
@all: ->
@initialize_collection()
@instances.concat()
@scope: (name, block) ->
@initialize_collection()
@Collection::[name] = (args...) ->
@filter (instance, index) -> block([instance].concat(args)...)
@delegate_collection_methods name
### Queries ###
@find: (id) ->
@initialize_collection()
@where({id}).first()
@find_or_create: (conditions={}) ->
@initialize_collection()
instance = @where(conditions).first()
return instance if instance?
new @(conditions)
class Base
# `concern` is a function extension added prior to this setup.
# It decorates the current class with both class and instance methods
# from the passed-in mixin.
@concern Model
contructor: ->
@constructor.register(this)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment