coffee-shop
Version:
Coffee Web Framework.
219 lines (208 loc) • 6.74 kB
text/coffeescript
sugar = require 'sugar'
module.exports = class CoffeeShop
@Table: class # like Arel
constructor: ->
@_table = ''
@_select = []
@_primary_key = 'id'
@_select = []
@_join = []
@_where = []
@_group = []
@_having = []
@_order = []
@_limit = 0
@_offset = 0
return
table: (@_table) ->
primary_key: (@_primary_key) ->
# chainables
_simple: (n) -> ->
a = arguments
if a.length >= 1 and all a, 's'
for k of a when word a[k]
a[k] = @escape_key a[k]
# ['a']
# ['a=1']
# ['a=1, b=1']
# ['a=1', 'b=1']
concat @["_#{n}"], a # raw sql
@
select: _Class::_simple 'select'
project: _Class::select # alias
join: ->
a = arguments
if word(a[0]) or a.length > 1
# ['table2']
# ['table2', 'table3']
# TODO: lookup relationship and build query here
for k of a
@_join.push "JOIN #{@escape_key a[k]}\n ON 1"
else if y a[0] is 's'
# ['LEFT OUTER JOIN table2 ON table1.id = table2.id']
@_join.push a[0] # raw sql
@
joins: _Class::join # alias
include: _Class::join # alias
# TODO: implement OR; use raw sql conditions for now
where: ->
a = arguments
s = sig a
if a.length >= 2 and all(a, 's') and a[0].indexOf('?') isnt -1
# ["customers.last LIKE 'a%?'", 'son']
# ["customers.? LIKE 'a%?'", 'last', 'son']
i = 0
@_where.push a[0].replace /\?/g, => @escape a[++i]
else if a.length >= 1 and all(a, 's')
# ["customers.first = 'bob'"]
# ["customers.first = 'bob'", "customers.last = 'anderson'"]
concat @_where, a # raw sql
else if s is 'o'
if a[0].length is `undefined` # hash
# [{'customers.first': 'bob'}]
# [{'customers.first': 'bob', 'customers.last': 'anderson'}]
# [{customers: { first: 'bob'}}]
r = (o, prefix='') =>
_r = []
for k of o
if typeof o[k] is 'object'
concat _r, r o[k], "#{@escape_key k}."
else
_r.push "#{prefix}#{if word(k) then @escape_key k else k} = #{@escape o[k]}"
return _r
concat @_where, r a[0]
@
group: _Class::_simple 'group'
having: _Class::_simple 'having'
order: _Class::_simple 'order'
limit: (@_limit) ->
@
take: _Class::limit # alias
offset: (@_offset) ->
@
skip: _Class::offset # alias
#TODO: improve escape functions
escape_key: (s) -> "`#{s.toString().replace(/`/g, '')}`"
escape: (s) ->
if typeof s is 'undefined' or s is null
'NULL'
else
"'"+s.toString().replace(/'/g,"\'")+"'"
toString: -> @toSql()
toSql: ->
"SELECT\n #{@_select.join(",\n ")}\n"+
"FROM #{@escape_key @_table}\n"+
@_join.join("\n")+
(if @_where.length then "WHERE\n #{@_where.join(" AND \n ")}\n" else '')+
(if @_group.length then "GROUP BY #{@_group.join(', ')}\n" else '')+
(if @_order.length then "ORDER BY #{@_order.join(', ')}\n" else '')+
(if @_having.length then "HAVING #{@_having.join(', ')}\n" else '')+
(if @_limit then "LIMIT #{@_limit}\n" else '')+
(if @_offset then "OFFSET #{@_offset}\n" else '')+
';'
@Model: class extends CoffeeShop.Table # like ActiveRecord
constructor: ->
super()
@id = null
@table @constructor.name.pluralize().toLowerCase()
@_attributes = {}
@_has_one = []
@_has_many = []
@_has_and_belongs_to_many = []
@_belongs_to = []
a = arguments
for k of a[0]
@[k] = a[0][k]
return
attr_accessible: (a) ->
for k of a
@_attributes[a[k]] = true
attributes: ->
attrs = {}
attrs[@_primary_key] = @[@_primary_key]
for own k of @ when y(@_attributes[k]) isnt 'u'
attrs[k] = @[k]
return attrs
serialize: ->
JSON.stringify @attributes()
all: (cb) ->
@execute_sql @toSql(), (err, records) =>
return cb err if err
for k of records
records[k] = new @constructor records[k]
cb null, records
return
return
first: (cb) ->
@limit 1
@all (err, results) ->
return cb err if err
cb null, results[0]
# may as well ask for all()
#last: (cb) ->
# @limit 1
# @all (err, results) ->
# return cb err if err
# cb null, results[results.length-1]
find: (id, cb) ->
@select '*'
@where id: id
@first cb
exists: (id, cb) ->
conditions = {}
conditions[@_primary_key] = id
@select('1').where(conditions).limit(1).first (err, result) ->
return cb err if err
cb null, !!result
save: (cb) ->
attrs = @attributes()
if @[@_primary_key]
pairs = []
for k, v of attrs when not (k is @_primary_key)
pairs.push "#{@escape_key k} = #{@escape v}"
sql = "UPDATE #{@escape_key @_table}\n"+
"SET #{pairs.join(', ')}\n"+
"WHERE #{@escape_key @_primary_key} = #{@escape @[@_primary_key]};"
else
names = []
values = []
for k, v of attrs when not (k is @_primary_key)
names.push @escape_key k
values.push @escape v
sql = "INSERT INTO #{@escape_key @_table} "+
"(#{names.join(', ')}) VALUES\n"+
"(#{values.join(', ')});"
@execute_sql sql, cb
@build: (o) ->
return instance = new @ o
@create: (o, cb) ->
instance = new @ o
#validate()
instance.save cb or ->
return instance
execute_sql: (sql, cb) ->
# db-proprietary; overridable
# e.g. app.db.exec sql, cb
console.log "would have executed sql:", sql
console.log "override .execute_sql() function to make it happen for real."
cb null
# pending
delete: ->
deleteAll: ->
has_one: (s) -> @_has_one.push s
has_many: (s) -> @_has_many.push s
has_and_belongs_to_many: (s) -> @_has_and_belongs_to_many.push s
belongs_to: (s) -> @_belongs_to.push s
validates_presence_of: ->
#mount_uploader: -> # should be third-party provided
validates_uniqueness_of: ->
validates_format_of: ->
transform_serialize: ->
update_attributes: ->
update_column: ->
after_create: ->
y=(v)->(typeof v)[0] # shorthand typeof
sig=(a)->s=''; s+=y(a[k]) for k of a; s # argument signature
word=(s)->y(s) is 's' and s.match(/^\w[\w\d]*$/) isnt null # when arguments = ['word']
concat=(a,b)->a[k] = b[k] for k of b; return
all=(a,t)->sig(a) is (new Array(a.length+1)).join t