UNPKG

11.7 kBJavaScriptView Raw
1var async = require('./async')
2var hide = require('./hideProperty')
3var Row = require('./row')
4var extend = require('./extend')
5
6//
7// opts.tableName
8// opts.db
9// opts.fromCache
10//
11var Table = module.exports = function Table(opts, cb) {
12 var self = this
13 if (!(self instanceof Table)) {
14 return new Table(opts, cb)
15 }
16
17 var tableName = opts.tableName
18
19 self.name = tableName
20 self.primaryKey = []
21 self.columns = []
22 self.fk = {}
23 self.db = opts.db
24
25 hide(self, 'db')
26
27 var models = self.db._opts.models
28 if (models && models[tableName] && typeof models[tableName] === 'function') {
29 var model = models[tableName]
30 self.Row = model
31 }
32
33 if (opts.fromCache) {
34 extend(self, self.db._opts.schema[tableName])
35 process.nextTick(cb)
36 return
37 }
38
39 async.each([
40 self.getColumns.bind(self),
41 self.getForeignKeys.bind(self),
42 self.getPrimaryKeyDefinition.bind(self)
43 ], function (fn, done) {
44 fn(done)
45 }, cb)
46}
47
48
49Table.prototype.insert = function(data, cb) {
50 return this.save(data, cb)
51}
52
53
54Table.prototype.count = function(opts, cb) {
55 var options = { count: true }
56 extend(options, opts)
57 return this.find(options, cb)
58}
59
60
61/**
62 * Find many records
63 * @param {Object} opts
64 * - where
65 * - order
66 * - limit
67 * - offset
68 * - params
69 * - hydrate
70 * - count {Boolean} if true, returns count instead of rows
71 */
72Table.prototype.find = function(opts, cb) {
73 var self = this
74 var iq = self.db._platform.identifierQuoteChar
75 opts = opts || {}
76 if (typeof opts === 'function') {
77 cb = opts
78 opts = {}
79 }
80
81 cb = cb || self.db._promiseResolver()
82
83 var select = iq + self.primaryKey.join(iq + ', ' + iq) + iq
84 if (opts.count) {
85 select = 'count(*) as count'
86 }
87 var sql = [
88 'SELECT ' + select,
89 'FROM ' + iq + self.name + iq,
90 'WHERE true'
91 ]
92 var values = opts.params || {}
93
94 if (opts) {
95
96 // where
97 if (isArray(opts.where)) {
98 opts.where.forEach(function(val, i) {
99 if (isString(val)) {
100 sql.push('AND ' + val)
101 }
102 });
103 } else if (isString(opts.where)) {
104 sql.push('AND ' + opts.where)
105 } else if (opts.where) {
106 Object.keys(opts.where).forEach(function(property) {
107 if (typeof opts.where[property] === 'undefined') {
108 return
109 }
110 sql.push('AND ' + iq + property + iq + " = :" + property)
111 values[property] = opts.where[property]
112 })
113 }
114
115 if (!opts.count) {
116 // order
117 if (opts.order) {
118 if (isArray(opts.order)) {
119 opts.order = opts.order.join(', ')
120 }
121 if (opts.order) {
122 sql.push('ORDER BY ' + opts.order)
123 }
124 }
125 // limit
126 if (opts.limit) {
127 sql.push('LIMIT ' + opts.limit)
128 }
129 // offset
130 if (isInteger(opts.offset)) {
131 sql.push('OFFSET ' + opts.offset)
132 }
133 }
134
135 }
136 sql = sql.join('\n')
137 self.db.execute(sql, values, function(err, rs) {
138 if (err) return cb(err)
139 if (opts.count) {
140 return cb(null, parseInt(rs[0].count, 10))
141 }
142 self.mget(rs, function(err, rows) {
143 if (err) return cb(err)
144 if (!opts.hydrate) {
145 return cb(null, rows)
146 }
147 // TODO: we can optimize multiple hydrations by using mget
148 async.each(rows, function (row, done) {
149 row.hydrate(opts.hydrate, done)
150 }, function (err) {
151 return cb(err, rows)
152 })
153 })
154 })
155
156 return cb.promise ? cb.promise : this
157}
158
159
160/**
161 * Find one record
162 * @param {Object} params
163 */
164Table.prototype.findOne = function(params, cb) {
165 params.limit = 1;
166 cb = cb || this.db._promiseResolver()
167 this.find(params, function(err, rs) {
168 if (err) return cb(err)
169 cb(null, rs[0])
170 })
171 return cb.promise ? cb.promise : this
172};
173
174
175/**
176 * @param {[type]} opts [description]
177 */
178Table.prototype.getColumns = function(cb) {
179 cb = cb || function(){}
180 var self = this
181 self.db._platform.getColumns(self.name, function(err, columns) {
182 if (err) return cb(err)
183 self.columns = columns
184 cb(null)
185 })
186}
187
188
189/**
190 *
191 */
192Table.prototype.getPrimaryKeyDefinition = function(cb) {
193 var self = this
194 self.db._platform.getPrimaryKeyDefinition(self.name, function(err, pk) {
195 if (err) return cb(err)
196 self.primaryKey = pk
197 cb(null)
198 })
199}
200
201
202/**
203 *
204 */
205Table.prototype.getForeignKeys = function(cb) {
206 var self = this
207 cb = cb || function(){}
208 self.db._platform.getForeignKeys(self.name, function(err, foreignKeys) {
209 if (err) return cb(err)
210 self.fk = foreignKeys
211 cb(null)
212 })
213}
214
215/**
216 * Get a row by primary key
217 * get(key)
218 * get(key, cb)
219 * get(key, params)
220 * get(key, params, cb)
221 *
222 * @param {string|object} key
223 * @param {object} params
224 * params.cacheOnly {boolean}
225 * params.hydrate {string|array}
226 * @param {Function} cb
227 */
228Table.prototype.get = function(key, params, cb) {
229 params = params || {}
230 if (typeof params === 'function') {
231 cb = params
232 params = {}
233 }
234 var self = table = this
235 cb = cb || self.db._promiseResolver()
236 var pkWhere = self.getPrimaryKeyWhereClause(key)
237 var iq = self.db._platform.identifierQuoteChar
238 var sql = [
239 'SELECT *',
240 'FROM ' + iq + self.name + iq,
241 'WHERE ' + pkWhere
242 ]
243 var memoMs = self.db._opts.memoize
244 var cacheKey = self.getCacheKey(key)
245 var memo = self.db._memo
246
247 // memoize so we're never running duplicate queries
248 if (memoMs) {
249 if (memo[cacheKey] && memo[cacheKey].timestamp + memoMs > Date.now()) {
250 // if this query is already in progress don't run it again
251 var interval = setInterval(function() {
252 if (!memo[cacheKey].queryInProgress) {
253 clearInterval(interval)
254 return instantiate(memo[cacheKey].data)
255 }
256 }, 1)
257 return cb.promise ? cb.promise : self
258 }
259 memo[cacheKey] = {
260 queryInProgress: true
261 }
262 }
263
264 var noCache = !self.db._opts.cache || typeof self.db._opts.cache.get !== 'function'
265 if (noCache) {
266 getFromDb()
267 return cb.promise ? cb.promise : self
268 }
269
270 // try to get from cache
271 var start = Date.now()
272 self.db._opts.cache.get(cacheKey, function(err, json) {
273 if (err) return cb(err)
274 if (json) {
275 var val
276 try {
277 val = JSON.parse(json)
278 } catch (e) {}
279
280 if (val) {
281 if (self.db._opts.debug) {
282 self.db._opts.debug(
283 '\n' + 'cache.get(' + cacheKey + ')\n' +
284 (Date.now() - start) + 'ms'
285 )
286 }
287 return instantiate(val)
288 }
289 }
290 getFromDb()
291 })
292 return cb.promise ? cb.promise : self
293
294 function instantiate(val) {
295 var row = self.instantiateRow(val)
296 if (!params.hydrate) {
297 return cb(null, row)
298 }
299 row.hydrate(params.hydrate, function (err) {
300 if (err) return cb(err)
301 return cb(null, row)
302 })
303 }
304
305 function getFromDb() {
306 if (params.cacheOnly) {
307 return cb(null, null)
308 }
309 self.db.execute(sql, function(err, rs) {
310 if (err) return cb(err)
311 var r = rs[0]
312 // memoize readOnly queries
313 if (memoMs) {
314 memo[cacheKey] = {
315 timestamp: Date.now(),
316 data: r
317 }
318 }
319 if (!r) {
320 var error = new Error('Not found: ' + self.name + ':' + key)
321 error.notFound = true
322 error.table = self.name
323 error.key = key
324 return cb(error)
325 }
326 // save to cache
327 if (self.db._opts.cache && typeof self.db._opts.cache.set === 'function') {
328 self.db._opts.cache.set(cacheKey, JSON.stringify(r), function(err) {
329 if (err) console.error(err)
330 })
331 }
332 instantiate(r)
333 })
334 }
335}
336
337
338Table.prototype.instantiateRow = function(data) {
339 var row = Row(data, this)
340 row._data = data
341 return row
342}
343
344
345Table.prototype.mget = function(keys, cb) {
346 var self = this
347 var rows = []
348 var cacheKeys = []
349 var iq = self.db._platform.identifierQuoteChar
350 cb = cb || self.db._promiseResolver()
351 async.forEachOf(keys, function(key, i, done) {
352 cacheKeys[i] = self.getCacheKey(key)
353 self.get(key, { cacheOnly: true }, function(err, obj) {
354 rows[i] = obj
355 done(err)
356 })
357 }, function(err) {
358 if (err) return cb(err)
359 // now get anything uncached from the db
360 var haveAllRows = true
361 var where = []
362 rows.forEach(function (row, i) {
363 if (row) return
364 haveAllRows = false
365 var pkWhere = self.getPrimaryKeyWhereClause(keys[i])
366 where.push('(' + pkWhere + ')')
367 })
368 if (haveAllRows) {
369 return cb(null, rows)
370 }
371 var sql = [
372 'SELECT *',
373 'FROM ' + iq + self.name + iq,
374 'WHERE (' + where.join(' OR ') + ')'
375 ]
376 self.db.execute(sql, function(err, rs) {
377 if (err) return cb(err)
378 rs.forEach(function (row) {
379 var cacheKey = self.getCacheKeyFromRow(row)
380 var i = cacheKeys.indexOf(cacheKey)
381 rows[i] = self.instantiateRow(row)
382 // save to cache
383 if (self.db._opts.cache && typeof self.db._opts.cache.set === 'function') {
384 self.db._opts.cache.set(cacheKey, JSON.stringify(row), function(err) {
385 if (err) console.error(err)
386 })
387 }
388 })
389 cb(null, rows)
390 })
391 })
392 return cb.promise ? cb.promise : this
393}
394
395
396/**
397 * @param array|object|int|string key the primary key.
398 */
399Table.prototype.getPrimaryKeyWhereClause = function(key) {
400 var self = this
401 var iq = self.db._platform.identifierQuoteChar
402 var criteria = ''
403 if (!isArray(key) && !isObject(key)) {
404 var temp = key
405 key = []
406 key[0] = temp
407 }
408 self.primaryKey.forEach(function(field, i) {
409 var val = key[field]
410 if (isArray(key)) {
411 val = key[i]
412 }
413 if (criteria.length > 0) {
414 criteria += ' AND '
415 }
416 if (!val && val !== 0) {
417 throw new Error('Invalid value specified for "' + self.name + '" primary key.')
418 }
419 criteria += iq + self.name + iq + '.' + iq + field + iq + " = " + self.db._query.queryValues(':val', {val: val})
420 })
421 return criteria;
422}
423
424
425Table.prototype.getCacheKey = function(key) {
426 var self = this
427 var cacheKey = ''
428 if (isObject(key)) {
429 Object.keys(key).forEach(function(prop) {
430 if (cacheKey) {
431 cacheKey = cacheKey + ','
432 }
433 cacheKey = cacheKey + key[prop]
434 })
435 } else if (isArray(key)) {
436 key.forEach(function(val) {
437 if (cacheKey) {
438 cacheKey = cacheKey + ','
439 }
440 cacheKey = cacheKey + val
441 })
442 } else {
443 cacheKey = key
444 }
445 cacheKey = self.name + ':' + cacheKey
446 return cacheKey
447}
448
449
450Table.prototype.getCacheKeyFromRow = function(row) {
451 var self = this
452 var pk = {}
453 self.primaryKey.forEach(function (column) {
454 pk[column] = row[column]
455 })
456 return self.getCacheKey(pk)
457}
458
459
460Table.prototype.invalidateMemo = function(key) {
461 var self = this
462 var cacheKey = self.getCacheKey(key)
463 delete self.db._memo[cacheKey]
464}
465
466
467Table.prototype.save = function(data, params, cb) {
468 params = params || {}
469 if (typeof params === 'function') {
470 cb = params
471 params = {}
472 }
473 var row = new Row(data, this)
474 return row.save(params, cb)
475}
476
477
478
479function isArray(obj) {
480 return Object.prototype.toString.call(obj) === '[object Array]'
481}
482
483function isString(obj) {
484 return toString.call(obj) === '[object String]'
485}
486
487function isObject(obj) {
488 return toString.call(obj) === '[object Object]'
489}
490
491// https://github.com/paulmillr/es6-shim
492// ES6 isInteger Polyfill https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger#Polyfill
493var isInteger = Number.isInteger || function(val) {
494 return typeof val === "number" &&
495 ! Number.isNaN(val) &&
496 Number.isFinite(val) &&
497 val > -9007199254740992 &&
498 val < 9007199254740992 &&
499 parseInt(val, 10) === val;
500}