2023-11-22 19:34:48 +01:00
const becca = require ( '../becca/becca.js' ) ;
const utils = require ( '../services/utils.js' ) ;
const eu = require ( './etapi_utils.js' ) ;
const mappers = require ( './mappers.js' ) ;
const noteService = require ( '../services/notes.js' ) ;
const TaskContext = require ( '../services/task_context.js' ) ;
const v = require ( './validators.js' ) ;
const searchService = require ( '../services/search/services/search.js' ) ;
const SearchContext = require ( '../services/search/search_context.js' ) ;
const zipExportService = require ( '../services/export/zip.js' ) ;
const zipImportService = require ( '../services/import/zip.js' ) ;
2022-01-07 19:33:59 +01:00
function register ( router ) {
2022-01-10 17:09:20 +01:00
eu . route ( router , 'get' , '/etapi/notes' , ( req , res , next ) => {
2022-01-08 13:18:12 +01:00
const { search } = req . query ;
if ( ! search ? . trim ( ) ) {
2023-06-05 09:23:42 +02:00
throw new eu . EtapiError ( 400 , 'SEARCH_QUERY_PARAM_MANDATORY' , "'search' query parameter is mandatory." ) ;
2022-01-08 13:18:12 +01:00
}
2022-01-12 21:14:12 +01:00
2022-01-08 13:18:12 +01:00
const searchParams = parseSearchParams ( req ) ;
2022-01-10 17:09:20 +01:00
const searchContext = new SearchContext ( searchParams ) ;
2022-01-12 21:14:12 +01:00
2022-01-10 17:09:20 +01:00
const searchResults = searchService . findResultsWithQuery ( search , searchContext ) ;
const foundNotes = searchResults . map ( sr => becca . notes [ sr . noteId ] ) ;
2022-01-12 21:14:12 +01:00
2022-01-10 17:09:20 +01:00
const resp = {
results : foundNotes . map ( note => mappers . mapNoteToPojo ( note ) )
} ;
2022-01-12 21:14:12 +01:00
2022-01-10 17:09:20 +01:00
if ( searchContext . debugInfo ) {
resp . debugInfo = searchContext . debugInfo ;
}
2022-01-12 21:14:12 +01:00
2022-01-10 17:09:20 +01:00
res . json ( resp ) ;
2022-01-08 13:18:12 +01:00
} ) ;
2022-01-10 17:09:20 +01:00
eu . route ( router , 'get' , '/etapi/notes/:noteId' , ( req , res , next ) => {
const note = eu . getAndCheckNote ( req . params . noteId ) ;
2022-01-07 19:33:59 +01:00
res . json ( mappers . mapNoteToPojo ( note ) ) ;
} ) ;
2022-01-12 19:32:23 +01:00
const ALLOWED _PROPERTIES _FOR _CREATE _NOTE = {
'parentNoteId' : [ v . mandatory , v . notNull , v . isNoteId ] ,
'title' : [ v . mandatory , v . notNull , v . isString ] ,
'type' : [ v . mandatory , v . notNull , v . isNoteType ] ,
'mime' : [ v . notNull , v . isString ] ,
'content' : [ v . notNull , v . isString ] ,
'notePosition' : [ v . notNull , v . isInteger ] ,
2022-02-16 05:49:49 +08:00
'prefix' : [ v . notNull , v . isString ] ,
2022-01-12 19:32:23 +01:00
'isExpanded' : [ v . notNull , v . isBoolean ] ,
2023-08-30 00:11:32 +02:00
'noteId' : [ v . notNull , v . isValidEntityId ] ,
'dateCreated' : [ v . notNull , v . isString , v . isLocalDateTime ] ,
'utcDateCreated' : [ v . notNull , v . isString , v . isUtcDateTime ]
2022-01-12 19:32:23 +01:00
} ;
2022-01-12 21:14:12 +01:00
2022-01-10 17:09:20 +01:00
eu . route ( router , 'post' , '/etapi/create-note' , ( req , res , next ) => {
2022-01-12 19:32:23 +01:00
const params = { } ;
2022-01-12 21:14:12 +01:00
2022-01-12 19:32:23 +01:00
eu . validateAndPatch ( params , req . body , ALLOWED _PROPERTIES _FOR _CREATE _NOTE ) ;
2022-01-07 19:33:59 +01:00
try {
const resp = noteService . createNewNote ( params ) ;
2022-01-12 21:14:12 +01:00
res . status ( 201 ) . json ( {
2022-01-07 19:33:59 +01:00
note : mappers . mapNoteToPojo ( resp . note ) ,
branch : mappers . mapBranchToPojo ( resp . branch )
} ) ;
}
catch ( e ) {
2022-01-12 19:32:23 +01:00
return eu . sendError ( res , 500 , eu . GENERIC _CODE , e . message ) ;
2022-01-07 19:33:59 +01:00
}
} ) ;
const ALLOWED _PROPERTIES _FOR _PATCH = {
2022-01-12 19:32:23 +01:00
'title' : [ v . notNull , v . isString ] ,
'type' : [ v . notNull , v . isString ] ,
2023-08-30 00:11:32 +02:00
'mime' : [ v . notNull , v . isString ] ,
'dateCreated' : [ v . notNull , v . isString , v . isLocalDateTime ] ,
'utcDateCreated' : [ v . notNull , v . isString , v . isUtcDateTime ]
2022-01-07 19:33:59 +01:00
} ;
2022-01-10 17:09:20 +01:00
eu . route ( router , 'patch' , '/etapi/notes/:noteId' , ( req , res , next ) => {
2023-06-05 09:23:42 +02:00
const note = eu . getAndCheckNote ( req . params . noteId ) ;
2022-01-07 23:06:04 +01:00
2022-01-07 19:33:59 +01:00
if ( note . isProtected ) {
2023-06-05 09:23:42 +02:00
throw new eu . EtapiError ( 400 , "NOTE_IS_PROTECTED" , ` Note ' ${ req . params . noteId } ' is protected and cannot be modified through ETAPI. ` ) ;
2022-01-07 19:33:59 +01:00
}
2022-01-07 23:06:04 +01:00
2022-01-10 17:09:20 +01:00
eu . validateAndPatch ( note , req . body , ALLOWED _PROPERTIES _FOR _PATCH ) ;
2022-01-12 19:32:23 +01:00
note . save ( ) ;
2022-01-07 23:06:04 +01:00
2022-01-07 19:33:59 +01:00
res . json ( mappers . mapNoteToPojo ( note ) ) ;
} ) ;
2022-01-10 17:09:20 +01:00
eu . route ( router , 'delete' , '/etapi/notes/:noteId' , ( req , res , next ) => {
2022-01-07 19:33:59 +01:00
const { noteId } = req . params ;
const note = becca . getNote ( noteId ) ;
2023-06-05 09:23:42 +02:00
if ( ! note ) {
2022-01-07 19:33:59 +01:00
return res . sendStatus ( 204 ) ;
}
2022-04-19 23:06:46 +02:00
note . deleteNote ( null , new TaskContext ( 'no-progress-reporting' ) ) ;
2022-01-07 19:33:59 +01:00
res . sendStatus ( 204 ) ;
} ) ;
2022-01-08 12:01:54 +01:00
2022-01-10 17:09:20 +01:00
eu . route ( router , 'get' , '/etapi/notes/:noteId/content' , ( req , res , next ) => {
const note = eu . getAndCheckNote ( req . params . noteId ) ;
2022-01-08 12:01:54 +01:00
2023-06-05 09:23:42 +02:00
if ( note . isProtected ) {
throw new eu . EtapiError ( 400 , "NOTE_IS_PROTECTED" , ` Note ' ${ req . params . noteId } ' is protected and content cannot be read through ETAPI. ` ) ;
}
2022-01-08 12:01:54 +01:00
const filename = utils . formatDownloadTitle ( note . title , note . type , note . mime ) ;
res . setHeader ( 'Content-Disposition' , utils . getContentDisposition ( filename ) ) ;
res . setHeader ( "Cache-Control" , "no-cache, no-store, must-revalidate" ) ;
res . setHeader ( 'Content-Type' , note . mime ) ;
res . send ( note . getContent ( ) ) ;
} ) ;
2022-01-10 17:09:20 +01:00
eu . route ( router , 'put' , '/etapi/notes/:noteId/content' , ( req , res , next ) => {
const note = eu . getAndCheckNote ( req . params . noteId ) ;
2022-01-08 12:01:54 +01:00
2023-06-05 09:23:42 +02:00
if ( note . isProtected ) {
throw new eu . EtapiError ( 400 , "NOTE_IS_PROTECTED" , ` Note ' ${ req . params . noteId } ' is protected and cannot be modified through ETAPI. ` ) ;
}
2022-01-08 12:01:54 +01:00
note . setContent ( req . body ) ;
2023-01-11 23:22:51 +01:00
2023-01-26 20:32:27 +01:00
noteService . asyncPostProcessContent ( note , req . body ) ;
2022-01-08 12:01:54 +01:00
return res . sendStatus ( 204 ) ;
} ) ;
2022-07-24 21:30:29 +02:00
eu . route ( router , 'get' , '/etapi/notes/:noteId/export' , ( req , res , next ) => {
const note = eu . getAndCheckNote ( req . params . noteId ) ;
const format = req . query . format || "html" ;
if ( ! [ "html" , "markdown" ] . includes ( format ) ) {
2023-06-05 09:23:42 +02:00
throw new eu . EtapiError ( 400 , "UNRECOGNIZED_EXPORT_FORMAT" , ` Unrecognized export format ' ${ format } ', supported values are 'html' (default) or 'markdown'. ` ) ;
2022-07-24 21:30:29 +02:00
}
const taskContext = new TaskContext ( 'no-progress-reporting' ) ;
// technically a branch is being exported (includes prefix), but it's such a minor difference yet usability pain
// (e.g. branchIds are not seen in UI), that we export "note export" instead.
const branch = note . getParentBranches ( ) [ 0 ] ;
zipExportService . exportToZip ( taskContext , branch , format , res ) ;
} ) ;
2023-01-11 23:18:51 +01:00
2023-06-15 23:21:40 +02:00
eu . route ( router , 'post' , '/etapi/notes/:noteId/import' , ( req , res , next ) => {
const note = eu . getAndCheckNote ( req . params . noteId ) ;
const taskContext = new TaskContext ( 'no-progress-reporting' ) ;
zipImportService . importZip ( taskContext , req . body , note ) . then ( importedNote => {
res . status ( 201 ) . json ( {
note : mappers . mapNoteToPojo ( importedNote ) ,
2023-11-03 01:11:47 +01:00
branch : mappers . mapBranchToPojo ( importedNote . getParentBranches ( ) [ 0 ] ) ,
2023-06-15 23:21:40 +02:00
} ) ;
} ) ; // we need better error handling here, async errors won't be properly processed.
} ) ;
2023-06-04 23:02:49 +02:00
eu . route ( router , 'post' , '/etapi/notes/:noteId/revision' , ( req , res , next ) => {
2023-01-11 23:18:51 +01:00
const note = eu . getAndCheckNote ( req . params . noteId ) ;
2023-06-04 23:01:40 +02:00
note . saveRevision ( ) ;
2023-01-11 23:18:51 +01:00
return res . sendStatus ( 204 ) ;
} ) ;
2023-06-05 09:23:42 +02:00
eu . route ( router , 'get' , '/etapi/notes/:noteId/attachments' , ( req , res , next ) => {
const note = eu . getAndCheckNote ( req . params . noteId ) ;
const attachments = note . getAttachments ( { includeContentLength : true } )
res . json (
attachments . map ( attachment => mappers . mapAttachmentToPojo ( attachment ) )
) ;
} ) ;
2022-01-07 19:33:59 +01:00
}
2022-01-08 13:18:12 +01:00
function parseSearchParams ( req ) {
const rawSearchParams = {
2023-01-11 23:08:57 +01:00
fastSearch : parseBoolean ( req . query , 'fastSearch' ) ,
includeArchivedNotes : parseBoolean ( req . query , 'includeArchivedNotes' ) ,
ancestorNoteId : req . query [ 'ancestorNoteId' ] ,
ancestorDepth : req . query [ 'ancestorDepth' ] , // e.g. "eq5"
orderBy : req . query [ 'orderBy' ] ,
orderDirection : parseOrderDirection ( req . query , 'orderDirection' ) ,
limit : parseInteger ( req . query , 'limit' ) ,
debug : parseBoolean ( req . query , 'debug' )
2022-01-08 13:18:12 +01:00
} ;
const searchParams = { } ;
for ( const paramName of Object . keys ( rawSearchParams ) ) {
if ( rawSearchParams [ paramName ] !== undefined ) {
searchParams [ paramName ] = rawSearchParams [ paramName ] ;
}
}
return searchParams ;
}
const SEARCH _PARAM _ERROR = "SEARCH_PARAM_VALIDATION_ERROR" ;
function parseBoolean ( obj , name ) {
if ( ! ( name in obj ) ) {
return undefined ;
}
if ( ! [ 'true' , 'false' ] . includes ( obj [ name ] ) ) {
2023-06-05 09:23:42 +02:00
throw new eu . EtapiError ( 400 , SEARCH _PARAM _ERROR , ` Cannot parse boolean ' ${ name } ' value ' ${ obj [ name ] } , allowed values are 'true' and 'false'. ` ) ;
2022-01-08 13:18:12 +01:00
}
return obj [ name ] === 'true' ;
}
2022-02-11 02:28:42 +08:00
function parseOrderDirection ( obj , name ) {
2022-01-08 13:18:12 +01:00
if ( ! ( name in obj ) ) {
return undefined ;
}
const integer = parseInt ( obj [ name ] ) ;
if ( ! [ 'asc' , 'desc' ] . includes ( obj [ name ] ) ) {
2023-06-05 09:23:42 +02:00
throw new eu . EtapiError ( 400 , SEARCH _PARAM _ERROR , ` Cannot parse order direction value ' ${ obj [ name ] } , allowed values are 'asc' and 'desc'. ` ) ;
2022-01-08 13:18:12 +01:00
}
return integer ;
}
2022-02-11 02:28:42 +08:00
function parseInteger ( obj , name ) {
2022-01-08 13:18:12 +01:00
if ( ! ( name in obj ) ) {
return undefined ;
}
const integer = parseInt ( obj [ name ] ) ;
if ( Number . isNaN ( integer ) ) {
2023-06-05 09:23:42 +02:00
throw new eu . EtapiError ( 400 , SEARCH _PARAM _ERROR , ` Cannot parse integer ' ${ name } ' value ' ${ obj [ name ] } '. ` ) ;
2022-01-08 13:18:12 +01:00
}
return integer ;
}
2022-01-07 19:33:59 +01:00
module . exports = {
register
2022-01-07 23:06:04 +01:00
} ;