2017-11-05 10:41:54 -05:00
const sql = require ( './sql' ) ;
2018-04-01 21:27:46 -04:00
const optionService = require ( './options' ) ;
2018-04-02 20:46:46 -04:00
const dateUtils = require ( './date_utils' ) ;
2018-04-01 21:27:46 -04:00
const syncTableService = require ( './sync_table' ) ;
2018-08-07 13:33:10 +02:00
const attributeService = require ( './attributes' ) ;
2018-08-01 09:26:02 +02:00
const eventService = require ( './events' ) ;
2018-03-31 10:51:37 -04:00
const repository = require ( './repository' ) ;
2018-11-06 14:23:49 +01:00
const cls = require ( '../services/cls' ) ;
2018-03-31 22:23:40 -04:00
const Note = require ( '../entities/note' ) ;
2018-11-08 11:08:16 +01:00
const Link = require ( '../entities/link' ) ;
2018-03-31 22:15:06 -04:00
const NoteRevision = require ( '../entities/note_revision' ) ;
2018-03-31 22:23:40 -04:00
const Branch = require ( '../entities/branch' ) ;
2018-08-21 13:49:45 +02:00
const Attribute = require ( '../entities/attribute' ) ;
2017-11-05 10:41:54 -05:00
2018-04-01 17:38:24 -04:00
async function getNewNotePosition ( parentNoteId , noteData ) {
2017-11-05 10:41:54 -05:00
let newNotePos = 0 ;
2018-04-01 17:38:24 -04:00
if ( noteData . target === 'into' ) {
2018-03-24 21:39:15 -04:00
const maxNotePos = await sql . getValue ( 'SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0' , [ parentNoteId ] ) ;
2017-11-05 10:41:54 -05:00
2018-01-27 17:18:19 -05:00
newNotePos = maxNotePos === null ? 0 : maxNotePos + 1 ;
}
2018-04-01 17:38:24 -04:00
else if ( noteData . target === 'after' ) {
const afterNote = await sql . getRow ( 'SELECT notePosition FROM branches WHERE branchId = ?' , [ noteData . target _branchId ] ) ;
2018-01-27 17:18:19 -05:00
2018-01-28 19:30:14 -05:00
newNotePos = afterNote . notePosition + 1 ;
2018-01-27 17:18:19 -05:00
2018-01-28 19:30:14 -05:00
// not updating dateModified to avoig having to sync whole rows
2018-03-24 21:39:15 -04:00
await sql . execute ( 'UPDATE branches SET notePosition = notePosition + 1 WHERE parentNoteId = ? AND notePosition > ? AND isDeleted = 0' ,
2018-01-28 19:30:14 -05:00
[ parentNoteId , afterNote . notePosition ] ) ;
2017-11-05 10:41:54 -05:00
2018-04-01 21:27:46 -04:00
await syncTableService . addNoteReorderingSync ( parentNoteId ) ;
2018-01-27 17:18:19 -05:00
}
else {
2018-04-01 17:38:24 -04:00
throw new Error ( 'Unknown target: ' + noteData . target ) ;
2018-01-27 17:18:19 -05:00
}
2018-03-31 22:23:40 -04:00
return newNotePos ;
}
2018-08-15 22:06:49 +02:00
async function triggerChildNoteCreated ( childNote , parentNote ) {
await eventService . emit ( eventService . CHILD _NOTE _CREATED , { childNote , parentNote } ) ;
}
2018-08-01 09:26:02 +02:00
async function triggerNoteTitleChanged ( note ) {
await eventService . emit ( eventService . NOTE _TITLE _CHANGED , note ) ;
}
2018-11-08 11:08:16 +01:00
/ * *
* FIXME : noteData has mandatory property "target" , it might be better to add it as parameter to reflect this
* /
2018-04-01 17:38:24 -04:00
async function createNewNote ( parentNoteId , noteData ) {
const newNotePos = await getNewNotePosition ( parentNoteId , noteData ) ;
2017-11-05 10:41:54 -05:00
2018-08-15 22:06:49 +02:00
const parentNote = await repository . getNote ( parentNoteId ) ;
2017-11-29 21:04:30 -05:00
2018-10-31 12:29:01 +01:00
if ( ! noteData . type ) {
if ( parentNote . type === 'text' || parentNote . type === 'code' ) {
noteData . type = parentNote . type ;
noteData . mime = parentNote . mime ;
}
else {
// inheriting note type makes sense only for text and code
noteData . type = 'text' ;
noteData . mime = 'text/html' ;
}
}
2018-08-15 22:06:49 +02:00
noteData . type = noteData . type || parentNote . type ;
noteData . mime = noteData . mime || parentNote . mime ;
2018-01-27 17:18:19 -05:00
2018-04-02 22:53:01 -04:00
const note = await new Note ( {
2018-04-01 17:38:24 -04:00
title : noteData . title ,
2018-11-08 11:08:16 +01:00
content : noteData . content ,
2018-04-01 17:38:24 -04:00
isProtected : noteData . isProtected ,
type : noteData . type || 'text' ,
mime : noteData . mime || 'text/html'
2018-04-02 22:53:01 -04:00
} ) . save ( ) ;
2018-01-26 21:31:52 -05:00
2018-04-02 22:53:01 -04:00
const branch = await new Branch ( {
2018-03-31 22:23:40 -04:00
noteId : note . noteId ,
2018-01-28 19:30:14 -05:00
parentNoteId : parentNoteId ,
notePosition : newNotePos ,
2018-04-08 13:14:30 -04:00
prefix : noteData . prefix ,
2018-04-01 17:38:24 -04:00
isExpanded : 0
2018-04-02 22:53:01 -04:00
} ) . save ( ) ;
2018-01-27 17:18:19 -05:00
2018-08-21 13:49:45 +02:00
for ( const attr of await parentNote . getAttributes ( ) ) {
if ( attr . name . startsWith ( "child:" ) ) {
await new Attribute ( {
noteId : note . noteId ,
type : attr . type ,
name : attr . name . substr ( 6 ) ,
value : attr . value ,
position : attr . position ,
isInheritable : attr . isInheritable
} ) . save ( ) ;
2018-09-01 13:18:55 +02:00
note . invalidateAttributeCache ( ) ;
2018-08-21 13:49:45 +02:00
}
}
2018-10-08 11:09:45 +02:00
await triggerNoteTitleChanged ( note ) ;
await triggerChildNoteCreated ( note , parentNote ) ;
2017-11-18 17:05:50 -05:00
return {
2018-04-01 11:05:09 -04:00
note ,
branch
2017-11-18 17:05:50 -05:00
} ;
2017-11-05 10:41:54 -05:00
}
2018-02-26 00:07:43 -05:00
async function createNote ( parentNoteId , title , content = "" , extraOptions = { } ) {
2018-02-26 20:47:34 -05:00
if ( ! parentNoteId ) throw new Error ( "Empty parentNoteId" ) ;
if ( ! title ) throw new Error ( "Empty title" ) ;
2018-04-01 11:42:12 -04:00
const noteData = {
2018-02-26 00:07:43 -05:00
title : title ,
content : extraOptions . json ? JSON . stringify ( content , null , '\t' ) : content ,
target : 'into' ,
2018-04-01 17:38:24 -04:00
isProtected : ! ! extraOptions . isProtected ,
2018-02-26 00:07:43 -05:00
type : extraOptions . type ,
2018-11-05 00:06:17 +01:00
mime : extraOptions . mime ,
dateCreated : extraOptions . dateCreated
2018-02-26 00:07:43 -05:00
} ;
2018-04-01 11:42:12 -04:00
if ( extraOptions . json && ! noteData . type ) {
noteData . type = "code" ;
noteData . mime = "application/json" ;
2018-02-26 00:07:43 -05:00
}
2018-04-03 22:15:28 -04:00
const { note , branch } = await createNewNote ( parentNoteId , noteData ) ;
2018-02-26 00:07:43 -05:00
2018-08-14 12:54:58 +02:00
for ( const attr of extraOptions . attributes || [ ] ) {
await attributeService . createAttribute ( {
noteId : note . noteId ,
type : attr . type ,
name : attr . name ,
value : attr . value
} ) ;
2018-02-26 00:07:43 -05:00
}
2018-04-03 22:15:28 -04:00
return { note , branch } ;
2018-02-26 00:07:43 -05:00
}
2018-03-31 10:51:37 -04:00
async function protectNoteRecursively ( note , protect ) {
2018-03-31 08:53:52 -04:00
await protectNote ( note , protect ) ;
2017-11-15 00:04:26 -05:00
2018-03-31 23:08:22 -04:00
for ( const child of await note . getChildNotes ( ) ) {
2018-03-31 10:51:37 -04:00
await protectNoteRecursively ( child , protect ) ;
2017-11-15 00:04:26 -05:00
}
}
2018-03-31 08:53:52 -04:00
async function protectNote ( note , protect ) {
2018-03-31 22:15:06 -04:00
if ( protect !== note . isProtected ) {
note . isProtected = protect ;
2017-11-15 00:04:26 -05:00
2018-04-01 17:38:24 -04:00
await note . save ( ) ;
2017-11-15 00:04:26 -05:00
}
2018-03-31 22:15:06 -04:00
await protectNoteRevisions ( note ) ;
2017-11-15 00:04:26 -05:00
}
2018-03-31 22:15:06 -04:00
async function protectNoteRevisions ( note ) {
2018-03-31 10:51:37 -04:00
for ( const revision of await note . getRevisions ( ) ) {
2018-03-31 22:15:06 -04:00
if ( note . isProtected !== revision . isProtected ) {
revision . isProtected = note . isProtected ;
2017-11-15 00:04:26 -05:00
2018-04-01 17:38:24 -04:00
await revision . save ( ) ;
2018-03-31 10:51:37 -04:00
}
2017-11-15 00:04:26 -05:00
}
}
2018-11-08 20:17:56 +01:00
function findImageLinks ( content , foundLinks ) {
const re = /src="\/api\/images\/([a-zA-Z0-9]+)\//g ;
let match ;
while ( match = re . exec ( content ) ) {
foundLinks . push ( {
type : 'image' ,
targetNoteId : match [ 1 ]
} ) ;
}
return match ;
}
function findHyperLinks ( content , foundLinks ) {
const re = /href="#root[a-zA-Z0-9\/]*\/([a-zA-Z0-9]+)\//g ;
let match ;
while ( match = re . exec ( content ) ) {
foundLinks . push ( {
type : 'hyper' ,
targetNoteId : match [ 1 ]
} ) ;
}
return match ;
}
2018-11-08 11:08:16 +01:00
async function saveLinks ( note ) {
2018-03-31 22:15:06 -04:00
if ( note . type !== 'text' ) {
2018-02-23 22:58:24 -05:00
return ;
}
2018-11-08 20:17:56 +01:00
const foundLinks = [ ] ;
findImageLinks ( note . content , foundLinks ) ;
findHyperLinks ( note . content , foundLinks ) ;
2018-11-08 11:08:16 +01:00
const existingLinks = await note . getLinks ( ) ;
2018-01-06 22:38:53 -05:00
2018-11-08 20:17:56 +01:00
for ( const foundLink of foundLinks ) {
const existingLink = existingLinks . find ( existingLink =>
existingLink . targetNoteId === foundLink . targetNoteId
&& existingLink . type === foundLink . type ) ;
2018-01-06 22:38:53 -05:00
2018-11-08 11:08:16 +01:00
if ( ! existingLink ) {
await new Link ( {
2018-03-31 22:15:06 -04:00
noteId : note . noteId ,
2018-11-08 20:17:56 +01:00
targetNoteId : foundLink . targetNoteId ,
type : foundLink . type
2018-04-02 22:53:01 -04:00
} ) . save ( ) ;
2018-01-06 22:38:53 -05:00
}
2018-11-08 11:08:16 +01:00
else if ( existingLink . isDeleted ) {
existingLink . isDeleted = false ;
await existingLink . save ( ) ;
}
// else the link exists so we don't need to do anything
2018-01-06 22:38:53 -05:00
}
2018-11-08 11:08:16 +01:00
// marking links as deleted if they are not present on the page anymore
2018-11-08 20:17:56 +01:00
const unusedLinks = existingLinks . filter ( existingLink => ! foundLinks . some ( foundLink =>
existingLink . targetNoteId === foundLink . targetNoteId
&& existingLink . type === foundLink . type ) ) ;
2018-01-06 22:38:53 -05:00
2018-11-08 11:08:16 +01:00
for ( const unusedLink of unusedLinks ) {
unusedLink . isDeleted = true ;
await unusedLink . save ( ) ;
2018-01-06 22:38:53 -05:00
}
}
2018-03-31 22:15:06 -04:00
async function saveNoteRevision ( note ) {
2017-12-10 12:56:59 -05:00
const now = new Date ( ) ;
2018-04-02 21:47:46 -04:00
const noteRevisionSnapshotTimeInterval = parseInt ( await optionService . getOption ( 'noteRevisionSnapshotTimeInterval' ) ) ;
2017-11-05 10:41:54 -05:00
2018-04-02 20:46:46 -04:00
const revisionCutoff = dateUtils . dateStr ( new Date ( now . getTime ( ) - noteRevisionSnapshotTimeInterval * 1000 ) ) ;
2017-11-05 10:41:54 -05:00
2018-08-31 18:22:53 +02:00
const existingNoteRevisionId = await sql . getValue (
2018-03-31 22:15:06 -04:00
"SELECT noteRevisionId FROM note_revisions WHERE noteId = ? AND dateModifiedTo >= ?" , [ note . noteId , revisionCutoff ] ) ;
2018-04-02 20:46:46 -04:00
const msSinceDateCreated = now . getTime ( ) - dateUtils . parseDateTime ( note . dateCreated ) . getTime ( ) ;
2018-03-31 22:15:06 -04:00
if ( note . type !== 'file'
2018-08-15 08:44:54 +02:00
&& ! await note . hasLabel ( 'disableVersioning' )
2018-08-31 18:22:53 +02:00
&& ! existingNoteRevisionId
2018-03-31 22:15:06 -04:00
&& msSinceDateCreated >= noteRevisionSnapshotTimeInterval * 1000 ) {
2018-04-02 22:53:01 -04:00
await new NoteRevision ( {
2018-03-31 22:15:06 -04:00
noteId : note . noteId ,
// title and text should be decrypted now
title : note . title ,
content : note . content ,
2018-04-08 11:57:14 -04:00
type : note . type ,
mime : note . mime ,
2018-08-07 11:38:00 +02:00
isProtected : false , // will be fixed in the protectNoteRevisions() call
2018-03-31 22:15:06 -04:00
dateModifiedFrom : note . dateModified ,
2018-04-02 20:46:46 -04:00
dateModifiedTo : dateUtils . nowDate ( )
2018-04-02 22:53:01 -04:00
} ) . save ( ) ;
2018-03-31 22:15:06 -04:00
}
}
2017-11-05 10:41:54 -05:00
2018-03-31 22:15:06 -04:00
async function updateNote ( noteId , noteUpdates ) {
const note = await repository . getNote ( noteId ) ;
2017-12-10 12:56:59 -05:00
2018-10-30 22:18:20 +01:00
if ( ! note . isContentAvailable ) {
throw new Error ( ` Note ${ noteId } is not available for change! ` ) ;
}
2018-03-31 22:15:06 -04:00
if ( note . type === 'file' ) {
// for update file, newNote doesn't contain file payloads
noteUpdates . content = note . content ;
}
2018-01-23 22:11:03 -05:00
2018-03-31 22:15:06 -04:00
await saveNoteRevision ( note ) ;
2017-11-05 10:41:54 -05:00
2018-08-01 09:26:02 +02:00
const noteTitleChanged = note . title !== noteUpdates . title ;
2018-03-31 22:15:06 -04:00
note . title = noteUpdates . title ;
2018-04-08 08:31:19 -04:00
note . setContent ( noteUpdates . content ) ;
2018-03-31 22:15:06 -04:00
note . isProtected = noteUpdates . isProtected ;
2018-04-01 17:38:24 -04:00
await note . save ( ) ;
2017-11-14 22:21:56 -05:00
2018-08-01 09:26:02 +02:00
if ( noteTitleChanged ) {
await triggerNoteTitleChanged ( note ) ;
}
2018-11-08 11:08:16 +01:00
await saveLinks ( note ) ;
2017-11-05 10:41:54 -05:00
2018-03-31 22:15:06 -04:00
await protectNoteRevisions ( note ) ;
2017-11-05 10:41:54 -05:00
}
2018-03-31 23:08:22 -04:00
async function deleteNote ( branch ) {
2018-08-07 11:38:00 +02:00
if ( ! branch || branch . isDeleted ) {
2018-01-14 21:39:21 -05:00
return ;
}
2018-08-30 22:38:34 +02:00
if ( branch . branchId === 'root' || branch . noteId === 'root' ) {
throw new Error ( "Can't delete root branch/note" ) ;
}
2018-03-31 23:08:22 -04:00
branch . isDeleted = true ;
await branch . save ( ) ;
2017-11-19 23:12:39 -05:00
2018-03-31 23:08:22 -04:00
const note = await branch . getNote ( ) ;
const notDeletedBranches = await note . getBranches ( ) ;
2017-11-05 10:41:54 -05:00
2018-03-31 23:08:22 -04:00
if ( notDeletedBranches . length === 0 ) {
note . isDeleted = true ;
2018-11-01 10:23:21 +01:00
// we don't reset content here, that's postponed and done later to give the user
// a chance to correct a mistake
2018-03-31 23:08:22 -04:00
await note . save ( ) ;
2017-11-05 10:41:54 -05:00
2018-07-26 09:08:51 +02:00
for ( const noteRevision of await note . getRevisions ( ) ) {
await noteRevision . save ( ) ;
}
2018-03-31 23:08:22 -04:00
for ( const childBranch of await note . getChildBranches ( ) ) {
await deleteNote ( childBranch ) ;
2017-11-19 23:12:39 -05:00
}
2018-08-15 15:27:22 +02:00
for ( const attribute of await note . getOwnedAttributes ( ) ) {
attribute . isDeleted = true ;
await attribute . save ( ) ;
}
2018-08-22 14:40:49 +02:00
2018-11-01 10:23:21 +01:00
for ( const attribute of await note . getTargetRelations ( ) ) {
2018-08-22 14:40:49 +02:00
attribute . isDeleted = true ;
await attribute . save ( ) ;
}
2017-11-19 23:12:39 -05:00
}
2017-11-05 10:41:54 -05:00
}
2018-11-01 10:23:21 +01:00
async function cleanupDeletedNotes ( ) {
const cutoffDate = new Date ( new Date ( ) . getTime ( ) - 48 * 3600 * 1000 ) ;
const notesForCleanup = await repository . getEntities ( "SELECT * FROM notes WHERE isDeleted = 1 AND content != '' AND dateModified <= ?" , [ dateUtils . dateStr ( cutoffDate ) ] ) ;
for ( const note of notesForCleanup ) {
2018-11-09 09:04:55 +01:00
note . content = null ;
2018-11-01 10:23:21 +01:00
await note . save ( ) ;
}
const notesRevisionsForCleanup = await repository . getEntities ( "SELECT note_revisions.* FROM notes JOIN note_revisions USING(noteId) WHERE notes.isDeleted = 1 AND note_revisions.content != '' AND notes.dateModified <= ?" , [ dateUtils . dateStr ( cutoffDate ) ] ) ;
for ( const noteRevision of notesRevisionsForCleanup ) {
2018-11-09 09:04:55 +01:00
noteRevision . content = null ;
2018-11-01 10:23:21 +01:00
await noteRevision . save ( ) ;
}
}
// first cleanup kickoff 5 minutes after startup
2018-11-06 14:23:49 +01:00
setTimeout ( cls . wrap ( cleanupDeletedNotes ) , 5 * 60 * 1000 ) ;
2018-11-01 10:23:21 +01:00
2018-11-06 14:23:49 +01:00
setInterval ( cls . wrap ( cleanupDeletedNotes ) , 4 * 3600 * 1000 ) ;
2018-11-01 10:23:21 +01:00
2017-11-05 10:41:54 -05:00
module . exports = {
createNewNote ,
2018-02-26 00:07:43 -05:00
createNote ,
2017-11-05 10:41:54 -05:00
updateNote ,
2017-11-15 00:04:26 -05:00
deleteNote ,
protectNoteRecursively
2017-11-05 17:58:55 -05:00
} ;