2018-03-09 22:59:12 +02:00
const { Database } = require ( 'lib/database.js' ) ;
2020-10-09 19:35:46 +02:00
const uuid = require ( 'lib/uuid' ) . default ;
2018-03-09 22:59:12 +02:00
const { time } = require ( 'lib/time-utils.js' ) ;
const Mutex = require ( 'async-mutex' ) . Mutex ;
2017-05-10 21:51:43 +02:00
2017-05-08 00:20:34 +02:00
class BaseModel {
2017-07-03 22:38:26 +02:00
static modelType ( ) {
2018-03-09 22:59:12 +02:00
throw new Error ( 'Must be overriden' ) ;
2017-07-03 22:38:26 +02:00
}
static tableName ( ) {
2018-03-09 22:59:12 +02:00
throw new Error ( 'Must be overriden' ) ;
2017-07-03 22:38:26 +02:00
}
2020-03-16 04:30:54 +02:00
static setDb ( db ) {
this . db _ = db ;
}
2017-06-17 20:40:08 +02:00
static addModelMd ( model ) {
if ( ! model ) return model ;
2019-07-29 15:43:53 +02:00
2017-06-17 20:40:08 +02:00
if ( Array . isArray ( model ) ) {
2020-03-14 01:46:14 +02:00
const output = [ ] ;
2017-06-17 20:40:08 +02:00
for ( let i = 0 ; i < model . length ; i ++ ) {
output . push ( this . addModelMd ( model [ i ] ) ) ;
}
return output ;
} else {
model = Object . assign ( { } , model ) ;
2017-07-03 21:50:45 +02:00
model . type _ = this . modelType ( ) ;
2017-06-17 20:40:08 +02:00
return model ;
}
}
2017-06-25 12:41:03 +02:00
static logger ( ) {
return this . db ( ) . logger ( ) ;
}
2017-05-12 21:54:06 +02:00
static useUuid ( ) {
return false ;
}
2017-05-15 21:10:00 +02:00
static byId ( items , id ) {
for ( let i = 0 ; i < items . length ; i ++ ) {
if ( items [ i ] . id == id ) return items [ i ] ;
}
return null ;
}
2020-06-02 23:27:36 +02:00
static defaultValues ( fieldNames ) {
const output = { } ;
for ( const n of fieldNames ) {
output [ n ] = this . db ( ) . fieldDefaultValue ( this . tableName ( ) , n ) ;
}
return output ;
}
2019-01-25 21:59:36 +02:00
static modelIndexById ( items , id ) {
for ( let i = 0 ; i < items . length ; i ++ ) {
if ( items [ i ] . id == id ) return i ;
}
return - 1 ;
}
2019-01-26 17:33:45 +02:00
static modelsByIds ( items , ids ) {
const output = [ ] ;
for ( let i = 0 ; i < items . length ; i ++ ) {
if ( ids . indexOf ( items [ i ] . id ) >= 0 ) {
output . push ( items [ i ] ) ;
}
}
return output ;
}
2018-05-09 10:53:47 +02:00
// Prefer the use of this function to compare IDs as it handles the case where
// one ID is null and the other is "", in which case they are actually considered to be the same.
static idsEqual ( id1 , id2 ) {
if ( ! id1 && ! id2 ) return true ;
if ( ! id1 && ! ! id2 ) return false ;
if ( ! ! id1 && ! id2 ) return false ;
return id1 === id2 ;
}
2018-02-27 22:51:07 +02:00
static modelTypeToName ( type ) {
for ( let i = 0 ; i < BaseModel . typeEnum _ . length ; i ++ ) {
const e = BaseModel . typeEnum _ [ i ] ;
if ( e [ 1 ] === type ) return e [ 0 ] . substr ( 5 ) . toLowerCase ( ) ;
}
2019-09-19 23:51:18 +02:00
throw new Error ( ` Unknown model type: ${ type } ` ) ;
2018-02-27 22:51:07 +02:00
}
2020-01-20 04:19:57 +02:00
static modelNameToType ( name ) {
for ( let i = 0 ; i < BaseModel . typeEnum _ . length ; i ++ ) {
const e = BaseModel . typeEnum _ [ i ] ;
const eName = e [ 0 ] . substr ( 5 ) . toLowerCase ( ) ;
if ( eName === name ) return e [ 1 ] ;
}
throw new Error ( ` Unknown model name: ${ name } ` ) ;
}
2017-05-19 21:32:49 +02:00
static hasField ( name ) {
2020-03-14 01:46:14 +02:00
const fields = this . fieldNames ( ) ;
2017-05-19 21:32:49 +02:00
return fields . indexOf ( name ) >= 0 ;
}
2017-07-16 14:53:59 +02:00
static fieldNames ( withPrefix = false ) {
2020-03-14 01:46:14 +02:00
const output = this . db ( ) . tableFieldNames ( this . tableName ( ) ) ;
2017-07-16 14:53:59 +02:00
if ( ! withPrefix ) return output ;
2017-07-19 21:15:55 +02:00
2020-03-14 01:46:14 +02:00
const p = withPrefix === true ? this . tableName ( ) : withPrefix ;
const temp = [ ] ;
2017-07-16 14:53:59 +02:00
for ( let i = 0 ; i < output . length ; i ++ ) {
2019-09-19 23:51:18 +02:00
temp . push ( ` ${ p } . ${ output [ i ] } ` ) ;
2017-07-16 14:53:59 +02:00
}
2017-07-19 21:15:55 +02:00
2017-07-16 14:53:59 +02:00
return temp ;
2017-05-18 21:58:01 +02:00
}
2017-12-05 00:58:42 +02:00
static fieldType ( name , defaultValue = null ) {
2020-03-14 01:46:14 +02:00
const fields = this . fields ( ) ;
2017-06-15 20:18:48 +02:00
for ( let i = 0 ; i < fields . length ; i ++ ) {
if ( fields [ i ] . name == name ) return fields [ i ] . type ;
}
2017-12-05 00:58:42 +02:00
if ( defaultValue !== null ) return defaultValue ;
2019-09-19 23:51:18 +02:00
throw new Error ( ` Unknown field: ${ name } ` ) ;
2017-06-15 20:18:48 +02:00
}
2017-05-20 00:16:50 +02:00
static fields ( ) {
return this . db ( ) . tableFields ( this . tableName ( ) ) ;
}
2018-10-31 02:35:57 +02:00
static removeUnknownFields ( model ) {
const newModel = { } ;
2020-03-14 01:46:14 +02:00
for ( const n in model ) {
2018-10-31 02:35:57 +02:00
if ( ! model . hasOwnProperty ( n ) ) continue ;
if ( ! this . hasField ( n ) && n !== 'type_' ) continue ;
newModel [ n ] = model [ n ] ;
}
return newModel ;
}
2017-05-20 00:16:50 +02:00
static new ( ) {
2020-03-14 01:46:14 +02:00
const fields = this . fields ( ) ;
const output = { } ;
2017-05-20 00:16:50 +02:00
for ( let i = 0 ; i < fields . length ; i ++ ) {
2020-03-14 01:46:14 +02:00
const f = fields [ i ] ;
2017-05-20 00:16:50 +02:00
output [ f . name ] = f . default ;
}
return output ;
}
2017-05-18 22:31:40 +02:00
static modOptions ( options ) {
if ( ! options ) {
options = { } ;
} else {
options = Object . assign ( { } , options ) ;
}
2018-03-09 22:59:12 +02:00
if ( ! ( 'isNew' in options ) ) options . isNew = 'auto' ;
if ( ! ( 'autoTimestamp' in options ) ) options . autoTimestamp = true ;
2020-07-24 02:45:15 +02:00
if ( ! ( 'userSideValidation' in options ) ) options . userSideValidation = false ;
2017-05-18 22:31:40 +02:00
return options ;
}
2017-12-20 21:45:25 +02:00
static count ( options = null ) {
if ( ! options ) options = { } ;
2019-09-19 23:51:18 +02:00
let sql = ` SELECT count(*) as total FROM \` ${ this . tableName ( ) } \` ` ;
if ( options . where ) sql += ` WHERE ${ options . where } ` ;
2019-07-29 15:43:53 +02:00
return this . db ( )
. selectOne ( sql )
2020-05-21 10:14:33 +02:00
. then ( r => {
2019-07-29 15:43:53 +02:00
return r ? r [ 'total' ] : 0 ;
} ) ;
2017-06-24 19:40:03 +02:00
}
2019-10-07 09:57:24 +02:00
static load ( id , options = null ) {
return this . loadByField ( 'id' , id , options ) ;
2017-06-11 23:11:14 +02:00
}
2017-07-17 20:46:09 +02:00
static shortId ( id ) {
2017-08-21 19:56:40 +02:00
return id . substr ( 0 , 5 ) ;
2017-07-17 20:46:09 +02:00
}
2017-07-10 22:47:01 +02:00
static loadByPartialId ( partialId ) {
2019-09-19 23:51:18 +02:00
return this . modelSelectAll ( ` SELECT * FROM \` ${ this . tableName ( ) } \` WHERE \` id \` LIKE ? ` , [ ` ${ partialId } % ` ] ) ;
2017-07-10 22:47:01 +02:00
}
2017-06-25 11:00:54 +02:00
static applySqlOptions ( options , sql , params = null ) {
if ( ! options ) options = { } ;
2017-07-26 20:36:16 +02:00
if ( options . order && options . order . length ) {
2020-03-14 01:46:14 +02:00
const items = [ ] ;
2017-07-26 20:36:16 +02:00
for ( let i = 0 ; i < options . order . length ; i ++ ) {
const o = options . order [ i ] ;
2020-05-27 18:21:46 +02:00
let item = ` \` ${ o . by } \` ` ;
2018-03-09 22:59:12 +02:00
if ( options . caseInsensitive === true ) item += ' COLLATE NOCASE' ;
2019-09-19 23:51:18 +02:00
if ( o . dir ) item += ` ${ o . dir } ` ;
2017-07-26 20:36:16 +02:00
items . push ( item ) ;
}
2019-09-19 23:51:18 +02:00
sql += ` ORDER BY ${ items . join ( ', ' ) } ` ;
2017-06-25 11:00:54 +02:00
}
2019-07-29 15:43:53 +02:00
2019-09-19 23:51:18 +02:00
if ( options . limit ) sql += ` LIMIT ${ options . limit } ` ;
2017-06-25 11:00:54 +02:00
return { sql : sql , params : params } ;
}
2017-08-20 16:29:18 +02:00
static async allIds ( options = null ) {
2020-03-14 01:46:14 +02:00
const q = this . applySqlOptions ( options , ` SELECT id FROM \` ${ this . tableName ( ) } \` ` ) ;
2017-08-20 16:29:18 +02:00
const rows = await this . db ( ) . selectAll ( q . sql , q . params ) ;
2020-05-21 10:14:33 +02:00
return rows . map ( r => r . id ) ;
2017-08-20 16:29:18 +02:00
}
2017-07-03 20:58:01 +02:00
static async all ( options = null ) {
2018-05-26 16:46:57 +02:00
if ( ! options ) options = { } ;
if ( ! options . fields ) options . fields = '*' ;
2020-01-20 04:19:57 +02:00
let sql = ` SELECT ${ this . db ( ) . escapeFields ( options . fields ) } FROM \` ${ this . tableName ( ) } \` ` ;
let params = [ ] ;
if ( options . where ) {
sql += ` WHERE ${ options . where } ` ;
if ( options . whereParams ) params = params . concat ( options . whereParams ) ;
}
2020-03-14 01:46:14 +02:00
const q = this . applySqlOptions ( options , sql , params ) ;
2020-01-20 04:19:57 +02:00
return this . modelSelectAll ( q . sql , q . params ) ;
2017-06-25 11:00:54 +02:00
}
2019-03-08 19:14:17 +02:00
static async byIds ( ids , options = null ) {
if ( ! ids . length ) return [ ] ;
if ( ! options ) options = { } ;
if ( ! options . fields ) options . fields = '*' ;
2019-09-19 23:51:18 +02:00
let sql = ` SELECT ${ this . db ( ) . escapeFields ( options . fields ) } FROM \` ${ this . tableName ( ) } \` ` ;
sql += ` WHERE id IN (" ${ ids . join ( '","' ) } ") ` ;
2020-03-14 01:46:14 +02:00
const q = this . applySqlOptions ( options , sql ) ;
2019-03-08 19:14:17 +02:00
return this . modelSelectAll ( q . sql ) ;
}
2017-07-03 20:58:01 +02:00
static async search ( options = null ) {
if ( ! options ) options = { } ;
2018-03-09 22:59:12 +02:00
if ( ! options . fields ) options . fields = '*' ;
2017-07-03 20:58:01 +02:00
2020-03-14 01:46:14 +02:00
const conditions = options . conditions ? options . conditions . slice ( 0 ) : [ ] ;
const params = options . conditionsParams ? options . conditionsParams . slice ( 0 ) : [ ] ;
2017-07-03 20:58:01 +02:00
if ( options . titlePattern ) {
2020-03-14 01:46:14 +02:00
const pattern = options . titlePattern . replace ( /\*/g , '%' ) ;
2018-03-09 22:59:12 +02:00
conditions . push ( 'title LIKE ?' ) ;
2017-07-03 20:58:01 +02:00
params . push ( pattern ) ;
}
2018-03-09 22:59:12 +02:00
if ( 'limit' in options && options . limit <= 0 ) return [ ] ;
2017-08-20 10:16:31 +02:00
2019-09-19 23:51:18 +02:00
let sql = ` SELECT ${ this . db ( ) . escapeFields ( options . fields ) } FROM \` ${ this . tableName ( ) } \` ` ;
if ( conditions . length ) sql += ` WHERE ${ conditions . join ( ' AND ' ) } ` ;
2017-07-03 20:58:01 +02:00
2020-03-14 01:46:14 +02:00
const query = this . applySqlOptions ( options , sql , params ) ;
2017-07-03 20:58:01 +02:00
return this . modelSelectAll ( query . sql , query . params ) ;
}
2017-06-17 20:12:09 +02:00
static modelSelectOne ( sql , params = null ) {
if ( params === null ) params = [ ] ;
2019-07-29 15:43:53 +02:00
return this . db ( )
. selectOne ( sql , params )
2020-05-21 10:14:33 +02:00
. then ( model => {
2019-07-29 15:43:53 +02:00
return this . filter ( this . addModelMd ( model ) ) ;
} ) ;
2017-06-17 20:12:09 +02:00
}
static modelSelectAll ( sql , params = null ) {
if ( params === null ) params = [ ] ;
2019-07-29 15:43:53 +02:00
return this . db ( )
. selectAll ( sql , params )
2020-05-21 10:14:33 +02:00
. then ( models => {
2019-07-29 15:43:53 +02:00
return this . filterArray ( this . addModelMd ( models ) ) ;
} ) ;
2017-06-17 20:12:09 +02:00
}
2018-01-08 22:04:44 +02:00
static loadByField ( fieldName , fieldValue , options = null ) {
if ( ! options ) options = { } ;
2018-03-09 22:59:12 +02:00
if ( ! ( 'caseInsensitive' in options ) ) options . caseInsensitive = false ;
2019-10-07 09:57:24 +02:00
if ( ! options . fields ) options . fields = '*' ;
let sql = ` SELECT ${ this . db ( ) . escapeFields ( options . fields ) } FROM \` ${ this . tableName ( ) } \` WHERE \` ${ fieldName } \` = ? ` ;
2018-03-09 22:59:12 +02:00
if ( options . caseInsensitive ) sql += ' COLLATE NOCASE' ;
2018-01-08 22:04:44 +02:00
return this . modelSelectOne ( sql , [ fieldValue ] ) ;
2017-05-19 21:12:09 +02:00
}
2020-06-28 19:00:51 +02:00
static loadByFields ( fields , options = null ) {
if ( ! options ) options = { } ;
if ( ! ( 'caseInsensitive' in options ) ) options . caseInsensitive = false ;
if ( ! options . fields ) options . fields = '*' ;
const whereSql = [ ] ;
const params = [ ] ;
for ( const fieldName in fields ) {
whereSql . push ( ` \` ${ fieldName } \` = ? ` ) ;
params . push ( fields [ fieldName ] ) ;
}
let sql = ` SELECT ${ this . db ( ) . escapeFields ( options . fields ) } FROM \` ${ this . tableName ( ) } \` WHERE ${ whereSql . join ( ' AND ' ) } ` ;
if ( options . caseInsensitive ) sql += ' COLLATE NOCASE' ;
return this . modelSelectOne ( sql , params ) ;
}
2017-07-02 17:46:03 +02:00
static loadByTitle ( fieldValue ) {
2019-09-19 23:51:18 +02:00
return this . modelSelectOne ( ` SELECT * FROM \` ${ this . tableName ( ) } \` WHERE \` title \` = ? ` , [ fieldValue ] ) ;
2017-07-02 17:46:03 +02:00
}
2017-05-23 22:36:19 +02:00
static diffObjects ( oldModel , newModel ) {
2020-03-14 01:46:14 +02:00
const output = { } ;
2017-12-14 22:21:36 +02:00
const fields = this . diffObjectsFields ( oldModel , newModel ) ;
for ( let i = 0 ; i < fields . length ; i ++ ) {
output [ fields [ i ] ] = newModel [ fields [ i ] ] ;
}
2018-03-09 22:59:12 +02:00
if ( 'type_' in newModel ) output . type _ = newModel . type _ ;
2017-12-14 22:21:36 +02:00
return output ;
}
static diffObjectsFields ( oldModel , newModel ) {
2020-03-14 01:46:14 +02:00
const output = [ ] ;
for ( const n in newModel ) {
2017-12-04 01:06:02 +02:00
if ( ! newModel . hasOwnProperty ( n ) ) continue ;
2018-03-09 22:59:12 +02:00
if ( n == 'type_' ) continue ;
2017-05-23 22:36:19 +02:00
if ( ! ( n in oldModel ) || newModel [ n ] !== oldModel [ n ] ) {
2017-12-14 22:21:36 +02:00
output . push ( n ) ;
2017-05-23 22:36:19 +02:00
}
}
return output ;
}
2017-12-04 01:06:02 +02:00
static modelsAreSame ( oldModel , newModel ) {
const diff = this . diffObjects ( oldModel , newModel ) ;
delete diff . type _ ;
return ! Object . getOwnPropertyNames ( diff ) . length ;
}
2018-02-07 21:02:07 +02:00
static saveMutex ( modelOrId ) {
const noLockMutex = {
2019-07-29 15:43:53 +02:00
acquire : function ( ) {
return null ;
} ,
2018-02-07 21:02:07 +02:00
} ;
if ( ! modelOrId ) return noLockMutex ;
2020-03-14 01:46:14 +02:00
const modelId = typeof modelOrId === 'string' ? modelOrId : modelOrId . id ;
2018-02-07 21:02:07 +02:00
if ( ! modelId ) return noLockMutex ;
let mutex = BaseModel . saveMutexes _ [ modelId ] ;
if ( mutex ) return mutex ;
mutex = new Mutex ( ) ;
BaseModel . saveMutexes _ [ modelId ] = mutex ;
return mutex ;
}
static releaseSaveMutex ( modelOrId , release ) {
if ( ! release ) return ;
if ( ! modelOrId ) return release ( ) ;
2020-03-14 01:46:14 +02:00
const modelId = typeof modelOrId === 'string' ? modelOrId : modelOrId . id ;
2018-02-07 21:02:07 +02:00
if ( ! modelId ) return release ( ) ;
2020-03-14 01:46:14 +02:00
const mutex = BaseModel . saveMutexes _ [ modelId ] ;
2018-02-07 21:02:07 +02:00
if ( ! mutex ) return release ( ) ;
delete BaseModel . saveMutexes _ [ modelId ] ;
release ( ) ;
}
2017-06-18 01:49:52 +02:00
static saveQuery ( o , options ) {
2019-07-29 15:43:53 +02:00
let temp = { } ;
2020-03-14 01:46:14 +02:00
const fieldNames = this . fieldNames ( ) ;
2017-05-20 00:16:50 +02:00
for ( let i = 0 ; i < fieldNames . length ; i ++ ) {
2020-03-14 01:46:14 +02:00
const n = fieldNames [ i ] ;
2017-05-20 00:16:50 +02:00
if ( n in o ) temp [ n ] = o [ n ] ;
}
2018-01-14 19:01:22 +02:00
// Remove fields that are not in the `fields` list, if provided.
2018-09-16 20:37:31 +02:00
// Note that things like update_time, user_updated_time will still
2018-01-14 19:01:22 +02:00
// be part of the final list of fields if autoTimestamp is on.
// id also will stay.
if ( ! options . isNew && options . fields ) {
const filtered = { } ;
2020-03-14 01:46:14 +02:00
for ( const k in temp ) {
2018-01-14 19:01:22 +02:00
if ( ! temp . hasOwnProperty ( k ) ) continue ;
2018-03-09 22:59:12 +02:00
if ( k !== 'id' && options . fields . indexOf ( k ) < 0 ) continue ;
2018-01-14 19:01:22 +02:00
filtered [ k ] = temp [ k ] ;
}
temp = filtered ;
}
2017-05-20 00:16:50 +02:00
o = temp ;
2018-01-14 19:11:44 +02:00
let modelId = temp . id ;
2017-06-18 01:49:52 +02:00
let query = { } ;
2017-05-12 21:54:06 +02:00
2017-08-20 22:11:32 +02:00
const timeNow = time . unixMs ( ) ;
2018-03-09 22:59:12 +02:00
if ( options . autoTimestamp && this . hasField ( 'updated_time' ) ) {
2017-08-20 22:11:32 +02:00
o . updated _time = timeNow ;
}
2017-12-05 00:58:42 +02:00
// The purpose of user_updated_time is to allow the user to manually set the time of a note (in which case
// options.autoTimestamp will be `false`). However note that if the item is later changed, this timestamp
// will be set again to the current time.
2018-09-16 20:37:31 +02:00
//
// The technique to modify user_updated_time while keeping updated_time current (so that sync can happen) is to
// manually set updated_time when saving and to set autoTimestamp to false, for example:
// Note.save({ id: "...", updated_time: Date.now(), user_updated_time: 1436342618000 }, { autoTimestamp: false })
2018-03-09 22:59:12 +02:00
if ( options . autoTimestamp && this . hasField ( 'user_updated_time' ) ) {
2017-08-20 22:11:32 +02:00
o . user _updated _time = timeNow ;
2017-05-19 21:32:49 +02:00
}
2017-06-18 01:49:52 +02:00
if ( options . isNew ) {
2017-05-19 21:12:09 +02:00
if ( this . useUuid ( ) && ! o . id ) {
2017-06-25 01:19:11 +02:00
modelId = uuid . create ( ) ;
o . id = modelId ;
2017-05-18 21:58:01 +02:00
}
2017-05-19 21:32:49 +02:00
2018-03-09 22:59:12 +02:00
if ( ! o . created _time && this . hasField ( 'created_time' ) ) {
2017-08-20 22:11:32 +02:00
o . created _time = timeNow ;
}
2018-03-09 22:59:12 +02:00
if ( ! o . user _created _time && this . hasField ( 'user_created_time' ) ) {
2017-10-22 19:12:16 +02:00
o . user _created _time = o . created _time ? o . created _time : timeNow ;
}
2018-03-09 22:59:12 +02:00
if ( ! o . user _updated _time && this . hasField ( 'user_updated_time' ) ) {
2017-10-22 19:12:16 +02:00
o . user _updated _time = o . updated _time ? o . updated _time : timeNow ;
2017-05-19 21:32:49 +02:00
}
2017-05-12 21:54:06 +02:00
query = Database . insertQuery ( this . tableName ( ) , o ) ;
2017-05-11 22:14:01 +02:00
} else {
2020-03-14 01:46:14 +02:00
const where = { id : o . id } ;
const temp = Object . assign ( { } , o ) ;
2017-05-12 21:54:06 +02:00
delete temp . id ;
2017-12-14 22:21:36 +02:00
2017-05-12 21:54:06 +02:00
query = Database . updateQuery ( this . tableName ( ) , temp , where ) ;
2017-05-11 22:14:01 +02:00
}
2017-05-12 21:54:06 +02:00
2017-06-25 01:19:11 +02:00
query . id = modelId ;
2017-07-16 18:06:05 +02:00
query . modObject = o ;
2017-05-18 21:58:01 +02:00
return query ;
}
2020-07-24 02:45:15 +02:00
static userSideValidation ( o ) {
2020-08-05 01:07:55 +02:00
if ( o . id && ! o . id . match ( /^[a-f0-9]{32}$/ ) ) {
2020-07-24 02:45:15 +02:00
throw new Error ( 'Validation error: ID must a 32-characters lowercase hexadecimal string' ) ;
}
const timestamps = [ 'user_updated_time' , 'user_created_time' ] ;
for ( const k of timestamps ) {
if ( ( k in o ) && ( typeof o [ k ] !== 'number' || isNaN ( o [ k ] ) || o [ k ] < 0 ) ) throw new Error ( 'Validation error: user_updated_time and user_created_time must be numbers greater than 0' ) ;
}
}
2018-02-07 21:02:07 +02:00
static async save ( o , options = null ) {
// When saving, there's a mutex per model ID. This is because the model returned from this function
// is basically its input `o` (instead of being read from the database, for performance reasons).
// This works well in general except if that model is saved simultaneously in two places. In that
// case, the output won't be up-to-date and would cause for example display issues with out-dated
// notes being displayed. This was an issue when notes were being synchronised while being decrypted
// at the same time.
const mutexRelease = await this . saveMutex ( o ) . acquire ( ) ;
2017-05-18 22:31:40 +02:00
options = this . modOptions ( options ) ;
2017-06-29 22:52:52 +02:00
options . isNew = this . isNew ( o , options ) ;
2017-05-18 21:58:01 +02:00
2017-12-05 00:58:42 +02:00
// Diff saving is an optimisation which takes a new version of the item and an old one,
// do a diff and save only this diff. IMPORTANT: When using this make sure that both
// models have been normalised using ItemClass.filter()
const isDiffSaving = options && options . oldItem && ! options . isNew ;
if ( isDiffSaving ) {
const newObject = BaseModel . diffObjects ( options . oldItem , o ) ;
newObject . type _ = o . type _ ;
newObject . id = o . id ;
o = newObject ;
}
2017-06-24 19:40:03 +02:00
o = this . filter ( o ) ;
2020-07-24 02:45:15 +02:00
if ( options . userSideValidation ) {
this . userSideValidation ( o ) ;
}
2017-06-11 23:11:14 +02:00
let queries = [ ] ;
2020-03-14 01:46:14 +02:00
const saveQuery = this . saveQuery ( o , options ) ;
const modelId = saveQuery . id ;
2017-05-18 21:58:01 +02:00
2017-06-11 23:11:14 +02:00
queries . push ( saveQuery ) ;
2017-07-16 14:53:59 +02:00
if ( options . nextQueries && options . nextQueries . length ) {
queries = queries . concat ( options . nextQueries ) ;
}
2018-02-07 21:02:07 +02:00
let output = null ;
try {
await this . db ( ) . transactionExecBatch ( queries ) ;
2017-05-18 21:58:01 +02:00
o = Object . assign ( { } , o ) ;
2017-11-28 00:50:46 +02:00
if ( modelId ) o . id = modelId ;
2018-03-09 22:59:12 +02:00
if ( 'updated_time' in saveQuery . modObject ) o . updated _time = saveQuery . modObject . updated _time ;
if ( 'created_time' in saveQuery . modObject ) o . created _time = saveQuery . modObject . created _time ;
if ( 'user_updated_time' in saveQuery . modObject ) o . user _updated _time = saveQuery . modObject . user _updated _time ;
if ( 'user_created_time' in saveQuery . modObject ) o . user _created _time = saveQuery . modObject . user _created _time ;
2017-06-17 20:40:08 +02:00
o = this . addModelMd ( o ) ;
2017-12-05 00:58:42 +02:00
if ( isDiffSaving ) {
2020-03-14 01:46:14 +02:00
for ( const n in options . oldItem ) {
2017-12-05 00:58:42 +02:00
if ( ! options . oldItem . hasOwnProperty ( n ) ) continue ;
if ( n in o ) continue ;
o [ n ] = options . oldItem [ n ] ;
}
}
2018-02-07 21:02:07 +02:00
output = this . filter ( o ) ;
2018-03-16 22:17:52 +02:00
} finally {
this . releaseSaveMutex ( o , mutexRelease ) ;
2018-02-07 21:02:07 +02:00
}
return output ;
2017-05-10 21:51:43 +02:00
}
2017-06-29 22:52:52 +02:00
static isNew ( object , options ) {
2019-07-29 15:43:53 +02:00
if ( options && 'isNew' in options ) {
2017-06-29 22:52:52 +02:00
// options.isNew can be "auto" too
if ( options . isNew === true ) return true ;
if ( options . isNew === false ) return false ;
}
return ! object . id ;
}
2017-06-24 19:40:03 +02:00
static filterArray ( models ) {
2020-03-14 01:46:14 +02:00
const output = [ ] ;
2017-06-24 19:40:03 +02:00
for ( let i = 0 ; i < models . length ; i ++ ) {
output . push ( this . filter ( models [ i ] ) ) ;
}
return output ;
}
static filter ( model ) {
2017-06-27 01:20:01 +02:00
if ( ! model ) return model ;
2020-03-14 01:46:14 +02:00
const output = Object . assign ( { } , model ) ;
for ( const n in output ) {
2017-06-27 01:20:01 +02:00
if ( ! output . hasOwnProperty ( n ) ) continue ;
2017-12-05 00:58:42 +02:00
2017-06-27 01:20:01 +02:00
// The SQLite database doesn't have booleans so cast everything to int
2017-12-05 00:58:42 +02:00
if ( output [ n ] === true ) {
output [ n ] = 1 ;
} else if ( output [ n ] === false ) {
2019-07-29 15:43:53 +02:00
output [ n ] = 0 ;
2017-12-05 00:58:42 +02:00
} else {
const t = this . fieldType ( n , Database . TYPE _UNKNOWN ) ;
if ( t === Database . TYPE _INT ) {
output [ n ] = ! n ? 0 : parseInt ( output [ n ] , 10 ) ;
}
}
2017-06-27 01:20:01 +02:00
}
2019-07-29 15:43:53 +02:00
2017-06-27 01:20:01 +02:00
return output ;
2017-06-24 19:40:03 +02:00
}
2019-09-13 00:16:42 +02:00
static delete ( id ) {
2018-03-09 22:59:12 +02:00
if ( ! id ) throw new Error ( 'Cannot delete object without an ID' ) ;
2019-09-19 23:51:18 +02:00
return this . db ( ) . exec ( ` DELETE FROM ${ this . tableName ( ) } WHERE id = ? ` , [ id ] ) ;
2017-05-16 22:25:19 +02:00
}
2017-07-11 20:17:23 +02:00
static batchDelete ( ids , options = null ) {
2017-11-28 02:22:38 +02:00
if ( ! ids . length ) return ;
2017-07-11 20:17:23 +02:00
options = this . modOptions ( options ) ;
2018-11-13 02:45:08 +02:00
const idFieldName = options . idFieldName ? options . idFieldName : 'id' ;
2019-09-19 23:51:18 +02:00
const sql = ` DELETE FROM ${ this . tableName ( ) } WHERE ${ idFieldName } IN (" ${ ids . join ( '","' ) } ") ` ;
2018-11-13 02:45:08 +02:00
return this . db ( ) . exec ( sql ) ;
2019-07-29 15:43:53 +02:00
}
2017-07-11 20:17:23 +02:00
2017-05-10 21:51:43 +02:00
static db ( ) {
2018-03-09 22:59:12 +02:00
if ( ! this . db _ ) throw new Error ( 'Accessing database before it has been initialised' ) ;
2019-07-29 15:43:53 +02:00
return this . db _ ;
2017-05-10 21:51:43 +02:00
}
2017-07-25 20:36:52 +02:00
static isReady ( ) {
return ! ! this . db _ ;
}
2017-05-08 00:20:34 +02:00
}
2020-10-18 22:52:10 +02:00
BaseModel . typeEnum _ = [
[ 'TYPE_NOTE' , 1 ] ,
[ 'TYPE_FOLDER' , 2 ] ,
[ 'TYPE_SETTING' , 3 ] ,
[ 'TYPE_RESOURCE' , 4 ] ,
[ 'TYPE_TAG' , 5 ] ,
[ 'TYPE_NOTE_TAG' , 6 ] ,
[ 'TYPE_SEARCH' , 7 ] ,
[ 'TYPE_ALARM' , 8 ] ,
[ 'TYPE_MASTER_KEY' , 9 ] ,
[ 'TYPE_ITEM_CHANGE' , 10 ] ,
[ 'TYPE_NOTE_RESOURCE' , 11 ] ,
[ 'TYPE_RESOURCE_LOCAL_STATE' , 12 ] ,
[ 'TYPE_REVISION' , 13 ] ,
[ 'TYPE_MIGRATION' , 14 ] ,
[ 'TYPE_SMART_FILTER' , 15 ] ,
[ 'TYPE_COMMAND' , 16 ] ,
] ;
2018-02-27 22:51:07 +02:00
for ( let i = 0 ; i < BaseModel . typeEnum _ . length ; i ++ ) {
const e = BaseModel . typeEnum _ [ i ] ;
BaseModel [ e [ 0 ] ] = e [ 1 ] ;
}
2017-06-06 22:01:43 +02:00
BaseModel . db _ = null ;
2019-09-13 00:16:42 +02:00
BaseModel . dispatch = function ( ) { } ;
2018-02-07 21:02:07 +02:00
BaseModel . saveMutexes _ = { } ;
2017-06-06 22:01:43 +02:00
2019-07-29 15:43:53 +02:00
module . exports = BaseModel ;