2020-11-05 18:58:23 +02:00
import paginationToSql from './models/utils/paginationToSql' ;
2021-01-29 20:45:11 +02:00
import Database from './database' ;
2021-01-22 19:41:11 +02:00
import uuid from './uuid' ;
import time from './time' ;
2021-05-10 11:32:31 +02:00
import JoplinDatabase , { TableField } from './JoplinDatabase' ;
2023-07-16 18:42:42 +02:00
import { LoadOptions , SaveOptions } from './models/utils/types' ;
2024-03-09 12:33:05 +02:00
import ActionLogger , { ItemActionType as ItemActionType } from './utils/ActionLogger' ;
2023-12-23 23:38:42 +02:00
import { SqlQuery } from './services/database/types' ;
2018-03-09 22:59:12 +02:00
const Mutex = require ( 'async-mutex' ) . Mutex ;
2017-05-10 21:51:43 +02:00
2020-11-05 18:58:23 +02:00
// New code should make use of this enum
export enum ModelType {
Note = 1 ,
Folder = 2 ,
Setting = 3 ,
Resource = 4 ,
Tag = 5 ,
NoteTag = 6 ,
Search = 7 ,
Alarm = 8 ,
MasterKey = 9 ,
ItemChange = 10 ,
NoteResource = 11 ,
ResourceLocalState = 12 ,
Revision = 13 ,
Migration = 14 ,
SmartFilter = 15 ,
Command = 16 ,
}
2021-10-14 17:34:53 +02:00
export interface DeleteOptions {
idFieldName? : string ;
changeSource? : number ;
deleteChildren? : boolean ;
2021-10-15 13:38:14 +02:00
// By default the application tracks item deletions, so that they can be
// applied to the remote items during synchronisation. However, in some
// cases, we don't want this. In particular when an item is deleted via
// sync, we don't need to track the deletion, because the operation doesn't
// need to applied again on next sync.
trackDeleted? : boolean ;
2023-07-23 16:57:55 +02:00
disableReadOnlyCheck? : boolean ;
2024-03-02 16:25:27 +02:00
2024-03-09 12:33:05 +02:00
// Used for logging
sourceDescription? : string | ActionLogger ;
2024-03-02 16:25:27 +02:00
// Tells whether the deleted item should be moved to the trash. By default
// it is permanently deleted.
toTrash? : boolean ;
// If the item is to be moved to the trash, tell what should be the new
// parent. By default the item will be moved at the root of the trash. Note
// that caller must ensure that this parent ID is a deleted folder.
toTrashParentId? : string ;
2021-10-14 17:34:53 +02:00
}
2017-05-08 00:20:34 +02:00
class BaseModel {
2020-11-05 18:58:23 +02:00
// TODO: This ancient part of Joplin about model types is a bit of a
// mess and should be refactored properly.
2020-11-12 21:13:28 +02:00
public static typeEnum_ : any [ ] = [
2020-11-05 18:58:23 +02:00
[ 'TYPE_NOTE' , ModelType . Note ] ,
[ 'TYPE_FOLDER' , ModelType . Folder ] ,
[ 'TYPE_SETTING' , ModelType . Setting ] ,
[ 'TYPE_RESOURCE' , ModelType . Resource ] ,
[ 'TYPE_TAG' , ModelType . Tag ] ,
[ 'TYPE_NOTE_TAG' , ModelType . NoteTag ] ,
[ 'TYPE_SEARCH' , ModelType . Search ] ,
[ 'TYPE_ALARM' , ModelType . Alarm ] ,
[ 'TYPE_MASTER_KEY' , ModelType . MasterKey ] ,
[ 'TYPE_ITEM_CHANGE' , ModelType . ItemChange ] ,
[ 'TYPE_NOTE_RESOURCE' , ModelType . NoteResource ] ,
[ 'TYPE_RESOURCE_LOCAL_STATE' , ModelType . ResourceLocalState ] ,
[ 'TYPE_REVISION' , ModelType . Revision ] ,
[ 'TYPE_MIGRATION' , ModelType . Migration ] ,
[ 'TYPE_SMART_FILTER' , ModelType . SmartFilter ] ,
[ 'TYPE_COMMAND' , ModelType . Command ] ,
2020-11-12 21:29:22 +02:00
] ;
2020-11-05 18:58:23 +02:00
2021-01-23 17:51:19 +02:00
public static TYPE_NOTE = ModelType . Note ;
public static TYPE_FOLDER = ModelType . Folder ;
public static TYPE_SETTING = ModelType . Setting ;
public static TYPE_RESOURCE = ModelType . Resource ;
public static TYPE_TAG = ModelType . Tag ;
public static TYPE_NOTE_TAG = ModelType . NoteTag ;
public static TYPE_SEARCH = ModelType . Search ;
public static TYPE_ALARM = ModelType . Alarm ;
public static TYPE_MASTER_KEY = ModelType . MasterKey ;
public static TYPE_ITEM_CHANGE = ModelType . ItemChange ;
public static TYPE_NOTE_RESOURCE = ModelType . NoteResource ;
public static TYPE_RESOURCE_LOCAL_STATE = ModelType . ResourceLocalState ;
public static TYPE_REVISION = ModelType . Revision ;
public static TYPE_MIGRATION = ModelType . Migration ;
public static TYPE_SMART_FILTER = ModelType . SmartFilter ;
public static TYPE_COMMAND = ModelType . Command ;
2020-11-05 18:58:23 +02:00
2023-06-30 11:30:29 +02:00
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
2020-12-30 12:54:00 +02:00
public static dispatch : Function = function ( ) { } ;
2020-11-12 21:13:28 +02:00
private static saveMutexes_ : any = { } ;
2020-11-05 18:58:23 +02:00
2021-01-29 20:45:11 +02:00
private static db_ : JoplinDatabase ;
2020-11-05 18:58:23 +02:00
2023-03-06 16:22:01 +02:00
public static modelType ( ) : ModelType {
2018-03-09 22:59:12 +02:00
throw new Error ( 'Must be overriden' ) ;
2017-07-03 22:38:26 +02:00
}
2023-03-06 16:22:01 +02:00
public static tableName ( ) : string {
2018-03-09 22:59:12 +02:00
throw new Error ( 'Must be overriden' ) ;
2017-07-03 22:38:26 +02:00
}
2023-03-06 16:22:01 +02:00
public static setDb ( db : any ) {
2020-03-16 04:30:54 +02:00
this . db_ = db ;
}
2023-03-06 16:22:01 +02:00
public static addModelMd ( model : any ) : any {
2017-06-17 20:40:08 +02:00
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 {
2023-06-01 13:02:36 +02:00
model = { . . . model } ;
2017-07-03 21:50:45 +02:00
model . type_ = this . modelType ( ) ;
2017-06-17 20:40:08 +02:00
return model ;
}
}
2023-03-06 16:22:01 +02:00
public static logger() {
2017-06-25 12:41:03 +02:00
return this . db ( ) . logger ( ) ;
}
2023-03-06 16:22:01 +02:00
public static useUuid() {
2017-05-12 21:54:06 +02:00
return false ;
}
2023-03-06 16:22:01 +02:00
public static byId ( items : any [ ] , id : string ) {
2017-05-15 21:10:00 +02:00
for ( let i = 0 ; i < items . length ; i ++ ) {
2022-07-23 09:31:32 +02:00
if ( items [ i ] . id === id ) return items [ i ] ;
2017-05-15 21:10:00 +02:00
}
return null ;
}
2023-03-06 16:22:01 +02:00
public static defaultValues ( fieldNames : string [ ] ) {
2020-11-12 21:13:28 +02:00
const output : any = { } ;
2020-06-02 23:27:36 +02:00
for ( const n of fieldNames ) {
output [ n ] = this . db ( ) . fieldDefaultValue ( this . tableName ( ) , n ) ;
}
return output ;
}
2023-03-06 16:22:01 +02:00
public static modelIndexById ( items : any [ ] , id : string ) {
2019-01-25 21:59:36 +02:00
for ( let i = 0 ; i < items . length ; i ++ ) {
2022-07-23 09:31:32 +02:00
if ( items [ i ] . id === id ) return i ;
2019-01-25 21:59:36 +02:00
}
return - 1 ;
}
2023-03-06 16:22:01 +02:00
public static modelsByIds ( items : any [ ] , ids : string [ ] ) {
2019-01-26 17:33:45 +02:00
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.
2023-03-06 16:22:01 +02:00
public static idsEqual ( id1 : string , id2 : string ) {
2018-05-09 10:53:47 +02:00
if ( ! id1 && ! id2 ) return true ;
if ( ! id1 && ! ! id2 ) return false ;
if ( ! ! id1 && ! id2 ) return false ;
return id1 === id2 ;
}
2023-03-06 16:22:01 +02:00
public static modelTypeToName ( type : number ) {
2018-02-27 22:51:07 +02:00
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
}
2023-03-06 16:22:01 +02:00
public static modelNameToType ( name : string ) {
2020-01-20 04:19:57 +02:00
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 } ` ) ;
}
2023-03-06 16:22:01 +02:00
public static hasField ( name : string ) {
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 ;
}
2023-06-30 10:11:26 +02:00
public 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
}
2023-03-06 16:22:01 +02:00
public static fieldType ( name : string , defaultValue : any = 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 ++ ) {
2022-07-23 09:31:32 +02:00
if ( fields [ i ] . name === name ) return fields [ i ] . type ;
2017-06-15 20:18:48 +02:00
}
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
}
2023-03-06 16:22:01 +02:00
public static fields ( ) : TableField [ ] {
2017-05-20 00:16:50 +02:00
return this . db ( ) . tableFields ( this . tableName ( ) ) ;
}
2023-03-06 16:22:01 +02:00
public static removeUnknownFields ( model : any ) {
2020-11-12 21:13:28 +02:00
const newModel : any = { } ;
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 ;
}
2023-03-06 16:22:01 +02:00
public static new ( ) {
2020-03-14 01:46:14 +02:00
const fields = this . fields ( ) ;
2020-11-12 21:13:28 +02:00
const output : any = { } ;
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 ;
}
2023-03-06 16:22:01 +02:00
public static modOptions ( options : any ) {
2017-05-18 22:31:40 +02:00
if ( ! options ) {
options = { } ;
} else {
2023-06-01 13:02:36 +02:00
options = { . . . options } ;
2017-05-18 22:31:40 +02:00
}
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 ;
}
2023-03-06 16:22:01 +02:00
public static count ( options : any = null ) {
2017-12-20 21:45:25 +02:00
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 )
2022-09-30 18:23:14 +02:00
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
2020-11-12 21:13:28 +02:00
. then ( ( r : any ) = > {
2019-07-29 15:43:53 +02:00
return r ? r [ 'total' ] : 0 ;
} ) ;
2017-06-24 19:40:03 +02:00
}
2023-06-13 19:06:16 +02:00
public static load ( id : string , options : LoadOptions = null ) {
2019-10-07 09:57:24 +02:00
return this . loadByField ( 'id' , id , options ) ;
2017-06-11 23:11:14 +02:00
}
2023-03-06 16:22:01 +02:00
public static shortId ( id : string ) {
2017-08-21 19:56:40 +02:00
return id . substr ( 0 , 5 ) ;
2017-07-17 20:46:09 +02:00
}
2023-03-06 16:22:01 +02:00
public static loadByPartialId ( partialId : string ) {
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
}
2024-01-06 19:21:51 +02:00
public static applySqlOptions ( options : LoadOptions , sql : string , params : any [ ] = null ) {
2017-06-25 11:00:54 +02:00
if ( ! options ) options = { } ;
2017-07-26 20:36:16 +02:00
if ( options . order && options . order . length ) {
2024-01-06 19:21:51 +02:00
sql += ` ORDER BY ${ paginationToSql ( {
limit : options.limit ,
order : options.order as any ,
page : 1 ,
2024-02-26 21:27:29 +02:00
caseInsensitive : options.caseInsensitive ,
2024-01-06 19:21:51 +02:00
} ) } ` ;
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 } ;
}
2023-03-06 16:22:01 +02:00
public static async allIds ( options : any = 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-11-12 21:13:28 +02:00
return rows . map ( ( r : any ) = > r . id ) ;
2017-08-20 16:29:18 +02:00
}
2024-01-06 19:21:51 +02:00
public static async all ( options : LoadOptions = 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 ( ) } \` ` ;
2020-11-12 21:13:28 +02:00
let params : any [ ] = [ ] ;
2020-01-20 04:19:57 +02:00
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
}
2024-03-02 16:25:27 +02:00
public static async byIds ( ids : string [ ] , options : LoadOptions = null ) {
2019-03-08 19:14:17 +02:00
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 ) ;
}
2023-03-06 16:22:01 +02:00
public static async search ( options : any = null ) {
2017-07-03 20:58:01 +02:00
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 ) ;
}
2023-12-23 23:38:42 +02:00
public static async modelSelectOne ( sqlOrSqlQuery : string | SqlQuery , params : any [ ] = null ) {
2017-06-17 20:12:09 +02:00
if ( params === null ) params = [ ] ;
2023-12-23 23:38:42 +02:00
let sql = '' ;
if ( typeof sqlOrSqlQuery !== 'string' ) {
sql = sqlOrSqlQuery . sql ;
params = sqlOrSqlQuery . params ? sqlOrSqlQuery . params : [ ] ;
} else {
sql = sqlOrSqlQuery ;
}
try {
const model = await this . db ( ) . selectOne ( sql , params ) ;
return this . filter ( this . addModelMd ( model ) ) ;
} catch ( error ) {
error . message = ` On query ${ JSON . stringify ( { sql , params } )}: ${ error . message } ` ;
throw error ;
}
2017-06-17 20:12:09 +02:00
}
2023-12-23 23:38:42 +02:00
public static async modelSelectAll < T = any > ( sqlOrSqlQuery : string | SqlQuery , params : any [ ] = null ) : Promise < T [ ] > {
2017-06-17 20:12:09 +02:00
if ( params === null ) params = [ ] ;
2023-12-23 23:38:42 +02:00
let sql = '' ;
if ( typeof sqlOrSqlQuery !== 'string' ) {
sql = sqlOrSqlQuery . sql ;
params = sqlOrSqlQuery . params ? sqlOrSqlQuery . params : [ ] ;
} else {
sql = sqlOrSqlQuery ;
}
try {
const models = await this . db ( ) . selectAll ( sql , params ) ;
return this . filterArray ( this . addModelMd ( models ) ) as T [ ] ;
} catch ( error ) {
error . message = ` On query ${ JSON . stringify ( { sql , params } )}: ${ error . message } ` ;
throw error ;
}
2017-06-17 20:12:09 +02:00
}
2023-12-13 21:24:58 +02:00
protected static selectFields ( options : LoadOptions ) : string {
if ( ! options || ! options . fields ) return '*' ;
return this . db ( ) . escapeFieldsToString ( options . fields ) ;
}
2023-06-13 19:06:16 +02:00
public static loadByField ( fieldName : string , fieldValue : any , options : LoadOptions = null ) {
2018-01-08 22:04:44 +02:00
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
}
2023-06-13 19:06:16 +02:00
public static loadByFields ( fields : any , options : LoadOptions = null ) {
2020-06-28 19:00:51 +02:00
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 ) ;
}
2023-03-06 16:22:01 +02:00
public static loadByTitle ( fieldValue : any ) {
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
}
2023-03-06 16:22:01 +02:00
public static diffObjects ( oldModel : any , newModel : any ) {
2020-11-12 21:13:28 +02:00
const output : any = { } ;
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 ;
}
2023-03-06 16:22:01 +02:00
public static diffObjectsFields ( oldModel : any , newModel : any ) {
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 ;
2022-07-23 09:31:32 +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 ;
}
2023-03-06 16:22:01 +02:00
public static modelsAreSame ( oldModel : any , newModel : any ) {
2017-12-04 01:06:02 +02:00
const diff = this . diffObjects ( oldModel , newModel ) ;
delete diff . type_ ;
return ! Object . getOwnPropertyNames ( diff ) . length ;
}
2023-03-06 16:22:01 +02:00
public static saveMutex ( modelOrId : any ) {
2018-02-07 21:02:07 +02:00
const noLockMutex = {
2020-11-12 21:13:28 +02:00
acquire : function ( ) : any {
2019-07-29 15:43:53 +02:00
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 ;
}
2023-06-30 11:30:29 +02:00
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
2023-03-06 16:22:01 +02:00
public static releaseSaveMutex ( modelOrId : any , release : Function ) {
2018-02-07 21:02:07 +02:00
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 ( ) ;
}
2023-03-06 16:22:01 +02:00
public static saveQuery ( o : any , options : any ) {
2020-11-12 21:13:28 +02:00
let temp : any = { } ;
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 ) {
2020-11-12 21:13:28 +02:00
const filtered : any = { } ;
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 ;
2020-11-12 21:13:28 +02:00
let query : any = { } ;
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 } ;
2023-06-01 13:02:36 +02:00
const temp = { . . . 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 ;
}
2023-03-06 16:22:01 +02:00
public static userSideValidation ( o : any ) {
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' ) ;
}
}
2023-07-16 18:42:42 +02:00
public static async save ( o : any , options : SaveOptions = null ) {
2018-02-07 21:02:07 +02:00
// 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 ) ;
2023-07-16 18:42:42 +02:00
const isNew = this . isNew ( o , options ) ;
options . isNew = isNew ;
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 ) ;
2023-06-01 13:02:36 +02:00
o = { . . . 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
}
2023-03-06 16:22:01 +02:00
public static isNew ( object : any , options : any ) {
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 ;
}
2023-03-06 16:22:01 +02:00
public static filterArray ( models : any [ ] ) {
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 ;
}
2023-03-06 16:22:01 +02:00
public static filter ( model : any ) {
2017-06-27 01:20:01 +02:00
if ( ! model ) return model ;
2023-06-01 13:02:36 +02:00
const output = { . . . model } ;
2020-03-14 01:46:14 +02:00
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
}
2024-03-09 12:33:05 +02:00
public static delete ( id : string , options? : DeleteOptions ) {
2018-03-09 22:59:12 +02:00
if ( ! id ) throw new Error ( 'Cannot delete object without an ID' ) ;
2024-03-09 12:33:05 +02:00
ActionLogger . from ( options ? . sourceDescription ) . log ( ItemActionType . Delete , 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
}
2024-03-09 12:33:05 +02:00
public static async batchDelete ( ids : string [ ] , options? : DeleteOptions ) {
2017-11-28 02:22:38 +02:00
if ( ! ids . length ) return ;
2024-03-09 12:33:05 +02:00
ActionLogger . from ( options ? . sourceDescription ) . log ( ItemActionType . Delete , ids ) ;
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 ( '","' ) } ") ` ;
2021-01-29 20:45:11 +02:00
await this . db ( ) . exec ( sql ) ;
2019-07-29 15:43:53 +02:00
}
2017-07-11 20:17:23 +02:00
2023-03-06 16:22:01 +02:00
public 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
}
2021-01-22 19:41:11 +02:00
// static isReady() {
// return !!this.db_;
// }
2017-05-08 00:20:34 +02:00
}
2018-02-27 22:51:07 +02:00
for ( let i = 0 ; i < BaseModel . typeEnum_ . length ; i ++ ) {
const e = BaseModel . typeEnum_ [ i ] ;
2020-11-05 18:58:23 +02:00
( BaseModel as any ) [ e [ 0 ] ] = e [ 1 ] ;
2018-02-27 22:51:07 +02:00
}
2020-11-05 18:58:23 +02:00
export default BaseModel ;