2024-04-07 16:56:45 +03:00
import becca = require ( '../becca/becca' ) ;
import utils = require ( '../services/utils' ) ;
import eu = require ( './etapi_utils' ) ;
import mappers = require ( './mappers' ) ;
import noteService = require ( '../services/notes' ) ;
import TaskContext = require ( '../services/task_context' ) ;
import v = require ( './validators' ) ;
import searchService = require ( '../services/search/services/search' ) ;
import SearchContext = require ( '../services/search/search_context' ) ;
import zipExportService = require ( '../services/export/zip' ) ;
import zipImportService = require ( '../services/import/zip' ) ;
import { Router } from 'express' ;
import { AppRequest } from '../routes/route-interface' ;
import { ParsedQs } from 'qs' ;
import { NoteParams } from '../services/note-interface' ;
import BNote = require ( '../becca/entities/bnote' ) ;
import { SearchParams } from '../services/search/services/types' ;
2024-07-16 21:43:04 +03:00
import { ValidatorMap } from './etapi-interface' ;
2024-04-07 16:56:45 +03:00
function register ( router : Router ) {
2022-01-10 17:09:20 +01:00
eu . route ( router , 'get' , '/etapi/notes' , ( req , res , next ) = > {
2024-04-07 14:56:22 +03:00
const { search } = req . query ;
2022-01-08 13:18:12 +01:00
2024-04-07 16:56:45 +03:00
if ( typeof search !== "string" || ! 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
2024-04-07 16:56:45 +03:00
const resp : any = {
results : foundNotes.map ( note = > mappers . mapNoteToPojo ( note ) ) ,
2022-01-10 17:09:20 +01:00
} ;
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 ) ) ;
} ) ;
2024-04-07 16:56:45 +03:00
const ALLOWED_PROPERTIES_FOR_CREATE_NOTE : ValidatorMap = {
2022-01-12 19:32:23 +01:00
'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
2024-04-07 14:56:22 +03:00
eu . route ( router , 'post' , '/etapi/create-note' , ( req , res , next ) = > {
2024-04-07 16:56:45 +03:00
const _params = { } ;
eu . validateAndPatch ( _params , req . body , ALLOWED_PROPERTIES_FOR_CREATE_NOTE ) ;
const params = _params as NoteParams ;
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 )
} ) ;
}
2024-04-07 16:56:45 +03:00
catch ( e : any ) {
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
} ;
2024-04-07 14:56:22 +03: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 ) ) ;
} ) ;
2024-04-07 14:56:22 +03:00
eu . route ( router , 'delete' , '/etapi/notes/:noteId' , ( req , res , next ) = > {
const { noteId } = req . params ;
2022-01-07 19:33:59 +01:00
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
2024-04-07 14:56:22 +03:00
eu . route ( router , 'get' , '/etapi/notes/:noteId/export' , ( req , res , next ) = > {
2022-07-24 21:30:29 +02:00
const note = eu . getAndCheckNote ( req . params . noteId ) ;
const format = req . query . format || "html" ;
2024-04-07 16:56:45 +03:00
if ( typeof format !== "string" || ! [ "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 ] ;
2024-04-07 16:56:45 +03:00
zipExportService . exportToZip ( taskContext , branch , format as "html" | "markdown" , res ) ;
2022-07-24 21:30:29 +02:00
} ) ;
2023-01-11 23:18:51 +01:00
2024-04-07 14:56:22 +03:00
eu . route ( router , 'post' , '/etapi/notes/:noteId/import' , ( req , res , next ) = > {
2023-06-15 23:21:40 +02:00
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.
} ) ;
2024-04-07 14:56:22 +03: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 ) ;
2024-04-07 14:56:22 +03:00
const attachments = note . getAttachments ( { includeContentLength : true } )
2023-06-05 09:23:42 +02:00
res . json (
attachments . map ( attachment = > mappers . mapAttachmentToPojo ( attachment ) )
) ;
} ) ;
2022-01-07 19:33:59 +01:00
}
2024-04-07 16:56:45 +03:00
function parseSearchParams ( req : AppRequest ) {
const rawSearchParams : SearchParams = {
2023-01-11 23:08:57 +01:00
fastSearch : parseBoolean ( req . query , 'fastSearch' ) ,
includeArchivedNotes : parseBoolean ( req . query , 'includeArchivedNotes' ) ,
2024-04-07 16:56:45 +03:00
ancestorNoteId : parseString ( req . query [ 'ancestorNoteId' ] ) ,
ancestorDepth : parseString ( req . query [ 'ancestorDepth' ] ) , // e.g. "eq5"
orderBy : parseString ( req . query [ 'orderBy' ] ) ,
// TODO: Check why the order direction was provided as a number, but it's a string everywhere else.
orderDirection : parseOrderDirection ( req . query , 'orderDirection' ) as unknown as string ,
2023-01-11 23:08:57 +01:00
limit : parseInteger ( req . query , 'limit' ) ,
debug : parseBoolean ( req . query , 'debug' )
2022-01-08 13:18:12 +01:00
} ;
2024-04-07 16:56:45 +03:00
const searchParams : SearchParams = { } ;
2022-01-08 13:18:12 +01:00
2024-04-07 16:56:45 +03:00
for ( const paramName of Object . keys ( rawSearchParams ) as ( keyof SearchParams ) [ ] ) {
2022-01-08 13:18:12 +01:00
if ( rawSearchParams [ paramName ] !== undefined ) {
2024-04-07 16:56:45 +03:00
( searchParams as any ) [ paramName ] = rawSearchParams [ paramName ] ;
2022-01-08 13:18:12 +01:00
}
}
return searchParams ;
}
const SEARCH_PARAM_ERROR = "SEARCH_PARAM_VALIDATION_ERROR" ;
2024-04-07 16:56:45 +03:00
function parseString ( value : string | ParsedQs | string [ ] | ParsedQs [ ] | undefined ) : string | undefined {
if ( typeof value === "string" ) {
return value ;
}
return undefined ;
}
function parseBoolean ( obj : any , name : string ) {
2022-01-08 13:18:12 +01:00
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' ;
}
2024-04-07 16:56:45 +03:00
function parseOrderDirection ( obj : any , name : string ) {
2024-04-16 21:10:39 +03:00
if ( ! ( name in obj ) ) {
return undefined ;
}
2022-01-08 13:18:12 +01:00
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 ;
}
2024-04-07 16:56:45 +03:00
function parseInteger ( obj : any , name : string ) {
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 ;
}
2024-04-07 16:56:45 +03:00
export = {
2022-01-07 19:33:59 +01:00
register
2022-01-07 23:06:04 +01:00
} ;