chocolate
Version:
A full stack Node.js web framework built using Coffeescript
1,267 lines (1,028 loc) • 63.3 kB
text/coffeescript
loadedDBs = {}
_defaults =
filename: 'db.log'
datadir: '.'
Log = ->
Fs = undefined
BufferStream = undefined
Path = undefined
_ = undefined
_exists = no
_flushed = []
_loaded = false
_db = {}
_ops = []
_op_index = {}
_paths = []
_path_index = {}
_modules = {}
_queue = ''
unless window? then _defaults.datadir = require('chocolate/server/document').datadir
_datadir = _defaults.datadir
_filename = _defaults.filename
_pathname = null
setTimeout (-> flush()), 100
init = (current = '', options = {}) ->
{module, var_str} = options
module ?= off
if typeof current is 'object' and current._db?
var_str = """
var _db = #{stringify current._db, mode:'json'},
_o = [#{(f.toString() for f in current._ops).join ','}],
_oi = #{stringify current._op_index, mode:'json'},
_p = #{stringify current._paths, mode:'json'},
_pi = #{stringify current._path_index, mode:'json'},
_m = #{stringify current._modules},
p, op;
"""
current = ''
else
var_str ?= """
var _db = {},
_o = [],
_oi = {},
_p = [],
_pi = {},
_m = {},
p, op;
"""
if options.alias
var_str += """
_o = _ops;
_oi = _op_index;
_p = _paths;
_pi = _path_index;
_m = _modules;
"""
if options.rebuild
var_str += """
#{if options.alias then '\n' else ''}if (_oi === null) {
var i, len;
_oi = {};
for (i = 0, len = _o.length; i < len; i++) {
_oi[_o[i].toString()] = i;
}
#{if options.alias then """
_op_index = _oi;""" else ""}
};
"""
func_str = """
var _ = function(oi, pi, data) {
var i, len, node, op, path, step, steps;
op = _o[oi];
path = _p[pi];
node = _db;
steps = path.split('.');
for (i = 0, len = steps.length; i < len; i++) {
step = steps[i];
if (node[step] == null) {
node[step] = {};
}
node = node[step];
}
op.call(node, data, _m);
};
"""
returns = if window?
"""
#{var_str}
#{func_str}
#{current}
"""
else
"""
#{var_str}
#{func_str}
module.exports = {
_db:_db,
_ops:_o,
_op_index:_oi,
_paths:_p,
_path_index:_pi,
_modules:_m
};
"""
if module
returns = """
(function () {
#{returns}
return {_db:_db,_ops:_o,_op_index:_oi,_paths:_p,_path_index:_pi,_modules:_m}
})()
"""
returns + '\n'
keyname = (name) ->
if name? then 'LateDB-' + name
else 'LateDB-' + filename()
filename = (name) ->
if name? then _filename = name
_filename
pathname = (name, path) ->
_datadir = path if path?
unless name?
unless _pathname? then _pathname = _datadir + '/' + _filename
_pathname
else
_datadir + '/' + name
ensure = (path) ->
node = _db
steps = path.split '.'
for step in steps when node?
unless node[step]? then node[step] = {}
node = node[step]
node
stringify = ->
return undefined if arguments.length is 0
object = arguments[0]
options = arguments[1] ? {}
if options.prettify is yes
tab = ' '
newline = '\n'
else
tab = newline = ''
array_as_object = no
switch options.mode
when 'json' then return JSON.stringify object, null, tab
when 'full' then array_as_object = yes
when 'js' then # default mode
write = if typeof options.write is 'function' then options.write else (str) -> str
concat = (w1, w2, w3) ->
if w1? and w2? and w3? then w1 + w2 + w3
else null
doit = (o, level, index) ->
level ?= 0
indent = if tab is '' then '' else (tab for [0...level]).join ''
ni = newline + indent
nit = ni + tab
type = Object.prototype.toString.apply o
sep = if index > 0 then ',' + nit else ''
switch type
when '[object Object]'
if o.constructor isnt {}.constructor and o.stringify? and typeof o.stringify is 'function' then write sep + o.stringify()
else
if o.constructor isnt {}.constructor and options.strict then throw "_.stringify in strict mode can only accept pure objects"
else
i = 0 ; concat write(sep + "#{if options.variable and level is 0 then 'var ' else '{'}#{nit}"), "#{(chunk for k,v of o when ((not options.filter? or (v.constructor in options.filter))) and (options.own isnt true or {}.hasOwnProperty.call(o, k)) and (chunk = write((if i++ > 0 then ',' + nit else '') + (if options.variable and level is 0 then k else "'" + k.toString().replace(/\'/g, "\\'") + "'") + (if options.variable and level is 0 then '=' else ':')) + doit(v, level+1))).join('')}", write("#{ni}#{if options.variable and level is 0 then ';' else '}'}")
when '[object Array]'
if array_as_object then i = 0 ; concat write(sep + "function () {#{nit}var a = []; var o = {#{nit}"), "#{(chunk for own k,v of o when (chunk = write((if i++ > 0 then ',' + nit else '') + k + ':') + doit(v, level+1))).join('')}", write("};#{nit}for (var k in o) {a[k] = o[k];} return a; }()") # necessary to serialize both indexes and properties
else concat write(sep + "[#{nit}"), "#{(chunk for v, i in o when (chunk = doit(v, level+1, i))?).join ('')}", write("#{ni}]")
when '[object Boolean]' then write sep + o
when '[object Number]' then write sep + o
when '[object Date]' then write sep + "new Date(#{o.valueOf()})"
when '[object Function]' then write sep + o.toString()
when '[object Math]' then write sep + 'Math'
when '[object String]' then write sep + "'#{o.replace /\'/g, '\\\''}'"
when '[object Undefined]' then write sep + 'void 0'
when '[object Null]' then write sep + 'null'
when '[object Buffer]', '[object SlowBuffer]'
write sep + (if o.length is 16 then require('chocolate/general/chocodash').Uuid.unparse o else o.toString())
doit(object)
require_db = (name) ->
Path ?= require 'path'
required = Path.resolve pathname(name ? _filename)
delete require.cache[required]
result = require required
result
load = (initial_queue) ->
_queue = ''
initial_queue ?= ''
if window?
current = localStorage.getItem keyname()
if current? and current isnt ""
{_db,_ops,_op_index,_paths,_path_index,_modules} = eval init current, module:on
_queue = initial_queue
else
Fs ?= require 'fs'
if Fs.existsSync pathname()
{_db,_ops,_op_index,_paths,_path_index,_modules} = require_db()
else
_queue = init()
_queue += (if _queue isnt '' then '\n' else '') + initial_queue
_exists = yes
_loaded = yes
loaded = -> _loaded
io = (path, data, op) ->
return _db unless typeof path is 'string'
if arguments.length is 3
updates = {}
updates[path] = {data, op}
return update updates
node = _db
steps = path.split '.'
for step in steps when node?
return null unless node[step]?
node = node[step]
node
write = (op_index, path_index, data) ->
switch arguments.length
when 0
# write timestamp
_queue += "// #{Date.now().valueOf()}\n"
when 2
if typeof path_index is 'string'
path = path_index
# write path registration
_queue += "p='#{path}';\n_pi[p]=_p.push(p)-1;\n"
else
op = path_index
# write op registration
_queue += "op=#{op.toString().replace /^\/\/.*(\n)*/mg, ''};\n_oi[op.toString()]=_o.push(op)-1;\n"
else
# write data update
_queue += "_(#{op_index},#{path_index},#{JSON.stringify(data)});\n"
update_one = (path, data, op) ->
op_hash = op.toString()
op_index = _op_index[op_hash]
unless op_index?
op_index = _op_index[op_hash] = -1 + _ops.push op
write op_index, op
path_index = _path_index[path]
unless path_index?
path_index = _path_index[path] = -1 + _paths.push path
write op_index, path
write op_index, path_index, data
op.call ensure(path), data, _modules
return
update = ->
# extract path, data, op from updates
# save on disk then in mem
# write update timestamp
# this = db node at path, then execute op with this and data
# look for op index then if found write _(op_id, data) in log
# if not found then calculate hash for op and then add op as ops(op_hash, op) in log then write _(op_id, data) in log
write()
if arguments.length is 2
if typeof arguments[1] is 'function'
[updates, op] = arguments
for path, data of updates then update_one path, data, op
else
[data, updates] = arguments
for path, op of updates then update_one path, data, op
else
[updates] = arguments
for path, {data, op} of updates then update_one path, data, op
return
flush = ->
if _queue.length > 0
unless _exists then load _queue
if window?
current = localStorage.getItem keyname()
localStorage.setItem keyname(), (current ? "") + _queue
setTimeout (-> (for f in _flushed then f()); return), 10
setTimeout (-> flush()), 100
else
Fs.appendFile pathname(), _queue, ->
for f in _flushed then f()
setTimeout (-> flush()), 100
_queue = ""
else
setTimeout (-> flush()), 100
return
forget = (callback) ->
_queue = ""
if window?
localStorage.removeItem keyname()
_exists = no
callback?()
else
Fs.unlink pathname(), ->
_exists = no
callback?()
return
clear = (options, callback) ->
if typeof options is 'function'
callback = options
options = null
_db = {}
_ops = []
_op_index = {}
_paths = []
_path_index = {}
_modules = {}
if options?.forget then forget(-> _loaded = no ; callback?())
else setTimeout (-> _loaded = no ; callback?()), 10
return
register = (name, service) ->
do_register = (n, f, p, m) ->
unless f?
m[n] = require name
_queue += "_m#{if p isnt '' then '["' + s + '"]' for s in p.split('.') else ''}[\"#{n}\"] = require('#{name}');\n"
else if typeof f is 'function'
m[n] = f
_queue += "_m#{if p isnt '' then '["' + s + '"]' for s in p.split('.') else ''}[\"#{n}\"] = (function (#{name}) { return #{f.toString().replace /^\/\/.*(\n)*/mg, ''}; })(_m[\"#{name}\"])\n"
else
m[n] = {}
_queue += "_m#{if p isnt '' then '["' + s + '"]' for s in p.split('.') else ''}[\"#{n}\"] = {};\n"
if f? then for own k,v of f then do_register k, v, p + (if p is '' then '' else '.') + n, m[n]
write()
do_register name, service, "", _modules
_modules[name]
upgrade = (name, service) ->
current = _modules[name]
timestamped = no
do_remove = (n, f, p, m) ->
if m[n]? and not f?
delete m[n]
unless timestamped then write() ; timestamped = yes
_queue += "delete _m#{if p isnt '' then '["' + s + '"]' for s in p.split('.') else ''}[\"#{n}\"];\n"
if m[n]? and f? then for own k of m[n] then do_remove k, f[k], p + (if p is '' then '' else '.') + n, m[n]
return
do_append = (n, f, p, m, force) ->
unless f?
if m[n] isnt req = require name
m[n] = req
unless timestamped then write() ; timestamped = yes
_queue += "_m#{if p isnt '' then '["' + s + '"]' for s in p.split('.') else ''}[\"#{n}\"] = require('#{name}');\n"
else if typeof f is 'function'
if force or not m[n]? or (m[n].toString().replace(/(^\/\/.*(\n)*)|(\r)/mg, '') isnt f.toString().replace(/(^\/\/.*(\n)*)|(\r)/mg, ''))
m[n] = f
unless timestamped then write() ; timestamped = yes
_queue += "_m#{if p isnt '' then '["' + s + '"]' for s in p.split('.') else ''}[\"#{n}\"] = (function (#{name}) { return #{f.toString().replace /^\/\/.*(\n)*/mg, ''}; })(_m[\"#{name}\"])\n"
force = yes
else
unless m[n]?
m[n] = {}
unless timestamped then write() ; timestamped = yes
_queue += "_m#{if p isnt '' then '["' + s + '"]' for s in p.split('.') else ''}[\"#{n}\"] = {};\n"
if f? then for own k,v of f then do_append k, v, p + (if p is '' then '' else '.') + n, m[n], force
return
do_remove name, service, "", _modules
do_append name, service, "", _modules
_modules[name]
module_ = (name) -> _modules[name]
parse = (context, time_limit, only_after) ->
only_after ?= no
context.after ?= no
if context.line[0] is '/' and context.line[1] is '/'
items = context.line.split ' '
time = parseInt items[1]
context.after = yes if time_limit? and time > time_limit
if (not only_after and context.after) or (only_after and not context.after)
return no
if context.log?
context.log += context.line + '\n'
yes
revert = (time, callback) ->
if window?
context = log: ''
current = localStorage.getItem keyname()
lines = current.split '\n'
for line, i in lines
context.line = line
context.index = i
unless parse context, time then break
localStorage.setItem keyname(), context.log
load() ; if callback? then setTimeout callback, 10
else
context = {}
BufferStream ?= require 'bufferstream'
buffer = new BufferStream size:'flexible', split:'\n'
i = 0
bytes = 0
buffer.on 'data', (chunk) ->
context.line = chunk.toString()
context.index = i++
unless parse context, time
readStream.unpipe()
else
bytes += 1 + Buffer.byteLength(context.line, 'utf8')
buffer.on 'unpipe', ->
clear ->
Fs.truncate pathname(), bytes, ->
load() ; callback?()
readStream = Fs.createReadStream pathname()
readStream.on 'open', -> readStream.pipe buffer
return
compact = (time, options, callback) ->
if typeof options is 'function'
callback = options
options = time
time = null
if typeof time is 'function'
callback = time
options = null
time = null
if window?
context = log: ''
current = localStorage.getItem keyname()
lines = current.split '\n'
context.compacted = no
only_after = no
for line, i in lines
context.line = line
context.index = i
should_compact = not parse context, time, only_after
should_compact = yes if not time? and i+1 is lines.length
if should_compact
unless context.compacted
context.compacted = yes
context.log = init eval init(context.log, module:on)
only_after = yes
parse context, time, only_after
localStorage.setItem keyname(), context.log
load() ; if callback? then setTimeout callback, 10
else
page_size = options?.page_size ? 128 * 1024
temp_filename = [(now = new Date()).getFullYear(), (if now.getMonth() + 1 < 10 then '0' else '') + (now.getMonth() + 1), (if now.getDate() < 10 then '0' else '') + now.getDate(), '-', process.pid, '-', (Math.random() * 0x100000000 + 1).toString(36)].join ''
copy = (context, options, callback) ->
if typeof options is 'function'
callback = options
options = null
options ?= {append: off}
if options.append and not time?
setTimeout (-> callback()), 10
return
BufferStream ?= require 'bufferstream'
buffer = new BufferStream size:'flexible', split:'\n'
i = 0
buffer.on 'data', (chunk) ->
context.line = chunk.toString()
context.index = i++
unless parse context, time, options.append
unless options.append then readStream.unpipe()
else
if context.log.length > page_size and context.flushable
context.flushable = no
Fs.appendFile pathname(temp_filename), context.log.substr(), -> context.flushable = yes
context.log = ''
buffer.on 'unpipe', ->
wait_io = ->
if context.flushable
Fs.appendFile pathname(temp_filename), context.log, ->
callback()
else setTimeout wait_io, 10
wait_io()
readStream = Fs.createReadStream pathname()
readStream.on 'open', -> readStream.pipe buffer
# 1-copy-cut part to be compacted
context = log: '', flushable: yes
copy context, ->
# 2-empty current db
clear ->
# 3-reload compacting db
compacting_db = require_db temp_filename
# 4-stringify it with a JSON stream writer
Fs.unlink pathname(temp_filename), ->
context = log: '', flushable: yes
writer = (chunk, index) ->
context.log += (if index > 0 then ',' else '') + chunk ? ''
if context.flushable and (context.log.length > page_size or not chunk?)
context.flushable = no
Fs.appendFile pathname(temp_filename), context.log.substr(), -> context.flushable = yes
context.log = ""
return
compacting_db._op_index = null
stringify compacting_db, {write:writer, strict:on, variable:on}
wait_io = ->
if context.flushable
context.log += "#{init '', var_str:'', alias:on, rebuild:on}\n"
Fs.appendFile pathname(temp_filename), context.log, ->
context = log: '', flushable: yes
# 5-concatene rest of the log to that file
copy context, append:on, ->
# 6-forget old log, rename file to be current log
Fs.unlink pathname(), ->
Fs.rename pathname(temp_filename), pathname(), ->
# 7-reload log
load() ; callback?()
else
setTimeout wait_io, 10
wait_io()
return
flushed = (callback) ->
if _queue.length > 0
_flushed.push callback
else
setTimeout (-> callback?()), 10
{load, loaded, io, write, update, flushed, register, upgrade, module:module_, clear, revert, compact, filename, pathname}
Log.exists = (name, datadir, callback) ->
if typeof datadir is 'function' then callback = datadir ; datadir = null
if typeof name in ['function', 'undefined'] then callback = name ; name = _defaults.filename
unless datadir? then datadir = _defaults.datadir
if window?
setTimeout (-> callback? localStorage.getItem('LateDB-' + name)?), 10
else
Fs = require 'fs'
Fs.exists datadir + '/' + name, (exists) -> callback? exists
return
lateDB = (name, path) ->
path ?= process?.cwd() if name?
name ?= _defaults.filename
db = loadedDBs[name]
if db? and not db.loaded() then db.load() ; if db.tables.count() > 0 then db.tables.init()
return db if db?
log = Log()
if name isnt _defaults.filename then log.filename name
if path? then log.pathname name, path
log.load()
db = (path, data, op) -> log.io.apply log, arguments
db.flushed = (callback) -> log.flushed.apply log, arguments
db.revert = (time, callback) -> log.revert.apply log, arguments
db.compact = (time, callback) -> log.compact.apply log, arguments
db.register = (name, service) -> log.register.apply log, arguments
db.module = (name) -> log.module.apply log, arguments
db.clear = (options, callback) -> log.clear.apply log, arguments
db.pathname = (options, callback) -> log.pathname.apply log, arguments
db.update = (updates) -> log.update.apply log, arguments
db.filename = (name) -> log.filename.apply log, arguments
db.load = -> log.load.apply log, arguments
db.loaded = -> log.loaded.apply log, arguments
db.world = do ->
get_world = -> log.module 'World'
select = (query) ->
return unless (World = get_world())?
{path, uuid} = query
if typeof query is 'string' then uuid = query
if path?
self = db path
return unless self?
if uuid? then self[uuid] else self
else
World[uuid]
insert = (path, uuid, object) ->
return unless path? and uuid?
unless get_world()?
log.register 'World', {}
db path, {uuid, object}, (o, {World}) ->
self = @[o.uuid] ?= {}
self[k] = v for k, v of o.object
World[o.uuid] = self
return
update = (path, uuid, object) ->
return unless get_world()? and path? and uuid? and object?
db path, {uuid, object}, (o, {World}) ->
self = World[o.uuid]
self[k] = v for k, v of o.object
return
delete_ = (path, uuid) ->
return unless get_world()? and path? and uuid?
db path, {uuid}, (o, {World}) ->
delete World[o.uuid]
delete @[o.uuid]
return
{select, insert, update, delete:delete_}
db.tables = do ->
Table = ->
lines: {}
length: 0
Table.insert = (table, line) ->
return unless line.id?
inc = if table.lines[line.id]? then 0 else 1
table.lines[line.id] = line
table.length += inc
Table.notify table, 'insert', line
Table.patchLine = (line, patch, do_prune=no) ->
pruned = no
for k,v of patch
if v? and v.constructor in [Object, Array] and line[k]?
if (pruned = Table.patchLine line[k], v, do_prune)
has_one = no; for kk of line[k] then has_one = yes; break
delete line[k] unless has_one
else
unless do_prune then line[k] = v
else if v is true
delete line[k]; pruned = yes
if line.constructor is Array and not line[line.length-1]? then line.pop()
pruned
Table.update = (table, update, prune) ->
return unless update?.id? or prune?.id?
line = table.lines[update?.id ? prune?.id]
return unless line?
Table.patchLine line, update
Table.patchLine line, prune, on if prune?
Table.notify table, 'update', update
Table.delete = (table, line) ->
return unless line?.id? or line?
dec = if table.lines[line.id ? line]? then 1 else 0
delete table.lines[line.id ? line]
table.length -= dec
Table.notify table, 'delete', line
Table.notify = (table, event, line) ->
if (observers = table.observers?[event])?
for observer in observers
observer table, line
Table.observe = (table, event, observer) ->
table.observers ?= {}
observers = table.observers[event] ?= []
observers.push observer
Table.toArray = (table) ->
arr = []
for id, line of table.lines then arr.push line
arr
if (log.module 'Table')? then log.upgrade 'Table', Table
List = ->
items: {}
first: null
last: null
List.upgrade = (table) ->
unless table.list?
table.list = List()
for id, line of table.lines then List.insert table, line
Table.observe table, 'insert', List.insert
Table.observe table, 'delete', List.delete
table
List.insert = (table, line) ->
list = table.list
item = list.last ? line: null, previous: null, next: null
unless list.first?
item.line = line
list.first = item
else
item.next = item = line:line, previous:item, next:null
list.last = item
list.items[line.id] = item
item
List.delete = (table, line) ->
list = table.list
deleted = list.items[line.id]
delete list.items[line.id]
{previous, next} = deleted
previous?.next = next
next?.previous = previous
if deleted is list.first then list.first = next
if deleted is list.last then list.last = previous
deleted
TransientTable = (table) -> table ? []
TransientTable.insert = (table, line) ->
table.push line
TransientTable.fromTable = (table, filter) ->
t_table = []
if table.lines?
if filter?
for id, line of table.lines when filter(line) then t_table.push line
else
for id, line of table.lines then t_table.push line
else
if filter?
for line in table when filter(line) then t_table.push line
else
t_table = table.slice()
t_table
IndexTable = ->
table = Table()
table.list = List()
table
IndexTable.insert = (table, line) ->
table.lines[line.id ? table.length] = line
table.length++
List.insert table, line
return
IndexTable.delete = (table, line) ->
if line.id?
delete table.lines[line.id]
else
for id, one_line of table.lines then if one_line is line then delete table.lines[id]; break
table.length--
List.delete table, line
return
Iterator = (table) ->
{
current: null
index: 0
has_next: ->
if table.lines?
if @current? then @current.next? else table.list.first?
else
table[@index + 1]
next: ->
if table.lines?
@index++
@current = if @current? then @current.next else table.list.first
@current?.line
else
@current = table[@index++]
}
Field = {}
Field.insert_from_line = (table, line) ->
for k, v of line
unless table.fields?[k] then alter table.name, add:k
return
Index = {}
Index.create = (table, fields_only) ->
unless table.index?
for id, line of table.lines then Index.create_line_fields_index table, line
unless fields_only is on
for id, line of table.lines then Index.create_line_cross_refs table, line
return
Index.create_line_fields_index = (table, line) ->
table.index ?= {}
table.index.id ?= {}
for field, value of line
if field in ['id', 'idx'] or field[-3..] in ['_id', 'Idx']
if field isnt 'id'
table.index[field] ?= {}
index_table = table.index[field][value] ?= IndexTable()
IndexTable.insert index_table, line
else
table.index.id[value] = line
return
Index.delete_line_fields_index = (table, line) ->
return unless table.index?
for field, value of line
if field in ['id', 'idx'] or field[-3..] in ['_id', 'Idx']
if field isnt 'id'
index_table = table.index[field]?[value]
if index_table? then IndexTable.delete index_table, line
else
delete table.index.id?[value]
return
Index.create_line_cross_refs = (table, line) ->
tables = db 'tables'
entities = db 'entities'
for field, value of line
# create `joins` (1 >> n link) and `ref` (n >> 1 link) fields
if field[-3..] is '_id'
linked_entity_name = field[...-3].toLowerCase()
joined_table_name = entities[linked_entity_name]
if (joined_table = tables[joined_table_name])?
unless joined_table.index? then Index.create joined_table, yes
if (joined_line = joined_table.index.id[value])?
joined_line_join_table = joined_line[table.name + '_joins'] ?= IndexTable()
IndexTable.insert joined_line_join_table, line
# add index infos in `joins` tables
joined_line_join_table.index ?= {}
for k, v of line when k in ['id', 'idx'] or k[-3..] in ['_id', 'Idx']
joined_line_join_table.index[k] ?= {}
index_in_joined_table = joined_line_join_table.index[k][v] ?= IndexTable()
IndexTable.insert index_in_joined_table, line
line[joined_table_name + '_ref'] = joined_line
return
Index.delete_line_cross_refs = (table, line) ->
tables = db 'tables'
entities = db 'entities'
for field, value of line
# delete `joins` (1 >> n link) and `ref` (n >> 1 link) fields
if field[-3..] is '_id'
linked_entity_name = field[...-3].toLowerCase()
joined_table_name = entities[linked_entity_name]
if (joined_table = tables[joined_table_name])?
continue unless joined_table.index?
joined_line = joined_table.index.id[value]
joined_line_join_table = joined_line[table.name + '_joins']
if joined_line_join_table then IndexTable.delete joined_line_join_table, line
# remove index infos in `joins` tables
continue unless joined_line_join_table.index?
for k, v of line when k in ['id', 'idx'] or k[-3..] in ['_id', 'Idx']
continue unless joined_line_join_table.index[k]?
index_in_joined_table = joined_line_join_table.index[k][v]
if index_in_joined_table then IndexTable.delete index_in_joined_table, line
delete line[joined_table_name + '_ref']
return
Index.insert_line = (table, line) ->
Index.create_line_fields_index table, line
Index.create_line_cross_refs table, line
return
Index.delete_line = (table, line) ->
Index.delete_line_fields_index table, line
Index.delete_line_cross_refs table, line
return
History = {}
History.insert = (table, line) ->
_query_defs = {}
_helpers = {}
clone = (value) ->
return value unless value? and value.constructor in [Object, Array]
set = (o, val) ->
for own k,v of val then switch Object.prototype.toString.apply(v)
when '[object Object]'
o[k] = {}; set o[k], v
when '[object Array]'
o[k] = []; set o[k], v
else o[k] = v
o
set (if value.constructor is Object then {} else []), value
extend = (object, values) ->
set = (o, val) ->
for own k,v of val
o ?= {}
if Object.prototype.toString.apply(o[k]) is '[object Object]' and Object.prototype.toString.apply(v) is '[object Object]' then set o[k], v
else o[k] = v
o
set object, values
entity = (name) ->
name_parts = name.split '_'
entity_name = []
for part in name_parts then entity_name.push part[...-1]
entity_name = entity_name.join '_'
list = (table_name) ->
tables = db 'tables'
return [] unless tables?
if table_name?
table = tables[table_name]
unless table.fields?
fields = {}
for id, line of table.lines
for k of line then fields[k] ?= on
alter table_name, {add: (k for k of fields)}
(k for k of table.fields)
else
(k for k of tables).sort()
get = (table_name, id, index) ->
table = db("tables.#{table_name}")
throw "can not get an item from non-existing table '#{table_name}'" unless table? and Object.prototype.toString.call(table) is '[object Object]'
if index?
table.index["#{index}_id"]?[id]?.lines
else
if id?.at?
idx = 0; for k,v of table.lines when idx++ is id.at then return v
else
if id?
table.lines[id]
else
table.lines
# create a table in lateDB
# name:
# table name following this protocol `entities_linkedEntities/singularEntity_singularLinkedEntity`
# plural form is supposed to be one letter added at the end of singular form
# if not, or if plural form is unregular, you must provide the singular entity name
#
# so it can be something like:
# `books`
# `books_versions`
# `categories/category`
create = (name, options = {}) ->
if count() is 0 then log.register 'Table', Table
[name, entity_name] = name.split '/'
entity_name ?= entity name
db 'entities', {entity_name, name}, (o) -> @[o.entity_name] = o.name
db 'tables', {name}, (o, {Table}) -> @[o.name] = Table()
alias = (part[0].toUpperCase() + part[1..] for part in entity_name.split('_')).join('') + '_'
db 'tables', {name, entity_name, alias, options}, (o) ->
table = @[o.name]
table.name = o.name
table.entity_name = o.entity_name
table.fields = {}
table.alias = o.alias
table.options = o.options
table.options.history ?= off
table.options.index ?= on
table.options.identity ?= off
List.upgrade db("tables.#{name}")
return
exists = (table_name) ->
db("tables.#{table_name}")?
alter = (name, options = {}) ->
if options.add?
db 'tables', {name, add:options.add}, (o) ->
table = @[o.name]
table.fields ?= {}
if o.add.constructor is Array
for add in o.add then table.fields[add] = on
else table.fields[o.add] = on
delete options.add
if options.drop?
db 'tables', {name, drop:options.drop}, (o) ->
table = @[o.name]
delete table.fields[o.drop] if table.fields?
for id, line of table.lines
unless table.options.index is off
Index.delete_line table, line
delete line[o.drop]
unless table.options.index is off
Index.insert_line table, line
delete options.drop
count = 0; for k of options then count++; break
if count > 0
db 'tables', {name, options}, (o) ->
table = @[o.name]
table.options = o.options
table.options.history ?= off
table.options.index ?= on
table.options.identity ?= off
return
drop = (table_name) ->
table = db("tables.#{table_name}")
db 'entities', {entity_name:table.entity_name}, (o) -> delete @[o.entity_name]
db 'tables', {name:table_name}, (o) -> delete @[o.name]
return
insert = (table_name, line) ->
table = db("tables.#{table_name}")
throw "can't insert line into non-existing table '#{table_name}'" unless table? and Object.prototype.toString.call(table) is '[object Object]'
if table.options.identity is on and not line.id? then line.id = db.tables.id(table_name)
throw "can't insert a line without an `id` field into table '#{table_name}'" unless line?.id?
db "tables.#{table_name}", line, (line, {Table}) -> Table.insert @, line
Field.insert_from_line table, line
unless table.options.index is off
Index.insert_line table, line
unless table.options.history is off
History.insert table, line
return line.id if table.options.identity is on
return
diff_update = (line, update, level) ->
u = {}; d = {}
level ?= 0
if level is 0 then if update.id? then u.id = d.id = update.id
found = u:no, d:no
for k,v of update when k isnt 'id' or level > 0
if v? and v.constructor in [Object, Array]
unless line[k]? then u[k] = clone(v); found.u = on
else
{u:uu, d:dd} = diff_update line[k], v, level + 1
if uu? then u[k] = uu; found.u = on
if dd? then d[k] = dd; found.d = on
else if v isnt line[k] then u[k] = v; found.u = on
if level > 0 then for k of line then unless update[k]? then d[k] = on; found.d = on
return {u:(if found.u then u else null), d:(if found.d then d else null)}
update = (table_name, line_update, options) ->
table = db("tables.#{table_name}")
throw "can not update line into non-existing table '#{table_name}'" unless table? and Object.prototype.toString.call(table) is '[object Object]'
line = table.lines[line_update?.id]
throw "can't update a non existing line (id:#{line_update?.id}) in #{table_name}'" unless line?
if options?.diff
{u:line_update, d:line_prune} = diff_update line, line_update
if line_update? or line_prune?
Field.insert_from_line table, line
unless table.options.index is off
Index.delete_line table, line
unless line_prune?
db "tables.#{table_name}", line_update, (line_update, {Table}) -> Table.update @, line_update
else
db "tables.#{table_name}", {line_update, line_prune}, ({line_update, line_prune}, {Table}) -> Table.update @, line_update, line_prune
unless table.options.index is off
Index.insert_line table, line
return
delete_ = (table_name, line_to_delete) ->
table = db("tables.#{table_name}")
throw "can not delete line from non-existing table '#{table_name}'" unless table? and Object.prototype.toString.call(table) is '[object Object]'
line = table.lines[line_to_delete?.id ? line_to_delete]
throw "can't update a non existing line (id:#{line_to_delete?.id ? line_to_delete}) in #{table_name}'" unless line?
db "tables.#{table_name}", {id: line_to_delete?.id ? line_to_delete}, (line, {Table}) -> Table.delete @, line
unless table.options.index is off
Index.delete_line table, line
return
id = (table_name) ->
table = db("tables.#{table_name}")
throw "can not get new id from non-existing table '#{table_name}'" unless table? and Object.prototype.toString.call(table) is '[object Object]'
id = table.id
unless id?
id = 0
for k, line of table.lines
if line.id > id then id = line.id
id += 1
db "tables.#{table_name}", {id}, ({id}) -> @id = id + 1
id
QueryIterator = class
constructor: (@table) ->
if @table?
@iterator = Iterator @table
else
@iterator = Iterator []
@continue = no
iterator: null
filtered: no
continue: yes
check: (field_def, table_name) ->
if table_name? and (index = field_def.indexOf table_name) is 0
if field_def[table_name.length] is '.' then field_def.substr(table_name.length + 1) else null
else
if (index = field_def.indexOf '.') >= 0 then null else field_def
next: (filter, keys, params, table_name, filter_is_where) ->
next = @iterator.next()
@continue = no unless @iterator.has_next()
return next if @filtered
return next unless filter?
if typeof filter is 'function'
return if filter.call(next, next, keys, table_name, _helpers) then next else null
else if filter_is_where
return if not (filter = filter[table_name])? or filter.call(next, params, _helpers) then next else null
joined_lines = @table
key_values = {}
if filter.keys? then for key, i in filter.keys
key_values[key] = keys[i]
if filter.clauses? then for clause in filter.clauses
filter_field = @check (clause.field ? clause), table_name
continue unless filter_field?
unless clause.field?
key_value = key_values[filter_field]
if @table.index?
joined_lines = @table.index[filter_field + '_id']?[key_value] ? @table.index[filter_field]?[key_value] ? vtable = TransientTable.fromTable @table, (line) -> line[filter_field] is key_value
else
joined_lines = TransientTable.fromTable @table, (line) -> line[filter_field + '_id'] is key_value or line[filter_field] is key_value
else
value = clause.value ? if clause.key? then key_values[clause.key] else null
continue unless value?
clause.oper ?= 'is'
new_joined_lines = if clause.oper is 'is' then @table.index?[filter_field]?[value] else null
if new_joined_lines? and filter_field is 'id'
new_joined_lines = TransientTable [new_joined_lines]
unless new_joined_lines?
new_joined_lines = TransientTable.fromTable joined_lines, (line) ->
switch clause.oper
when 'is'
line[filter_field] is value
when 'isnt'
line[filter_field] isnt value
else
no
joined_lines = new_joined_lines
if joined_lines is @table
@filtered = yes
return next
@table = joined_lines
@filtered = yes
@iterator = Iterator @table
next = @iterator.next()
@continue = no unless @iterator.has_next()
return next
query = (entity_name, keys, query_name) ->
tables = db 'tables'
entities = db 'e