2024-07-18 21:35:17 +03:00
import becca from "../becca/becca.js" ;
import utils from "../services/utils.js" ;
import eu from "./etapi_utils.js" ;
import mappers from "./mappers.js" ;
import noteService from "../services/notes.js" ;
import TaskContext from "../services/task_context.js" ;
import v from "./validators.js" ;
import searchService from "../services/search/services/search.js" ;
import SearchContext from "../services/search/search_context.js" ;
import zipExportService from "../services/export/zip.js" ;
import zipImportService from "../services/import/zip.js" ;
2025-01-09 18:36:24 +02:00
import { type Request , Router } from "express" ;
import type { ParsedQs } from "qs" ;
import type { NoteParams } from "../services/note-interface.js" ;
import type { SearchParams } from "../services/search/services/types.js" ;
import type { ValidatorMap } from "./etapi-interface.js" ;
2024-04-07 16:56:45 +03:00
function register ( router : Router ) {
2025-01-09 18:07:02 +02: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 ( ) ) {
2025-01-09 18:07:02 +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 ) ;
2025-01-09 18:07:02 +02:00
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 = {
2025-01-09 18:07:02 +02:00
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
} ) ;
2025-01-09 18:07:02 +02:00
eu . route ( router , "get" , "/etapi/notes/:noteId" , ( req , res , next ) = > {
2022-01-10 17:09:20 +01:00
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 = {
2025-01-09 18:07:02 +02: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 ] ,
prefix : [ v . notNull , v . isString ] ,
isExpanded : [ v . notNull , v . isBoolean ] ,
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
2025-01-09 18:07:02 +02: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 )
} ) ;
2025-01-09 18:07:02 +02: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 = {
2025-01-09 18:07:02 +02:00
title : [ v . notNull , v . isString ] ,
type : [ v . notNull , v . isString ] ,
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
} ;
2025-01-09 18:07:02 +02: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 ) ) ;
} ) ;
2025-01-09 18:07:02 +02:00
eu . route ( router , "delete" , "/etapi/notes/:noteId" , ( req , res , next ) = > {
2024-04-07 14:56:22 +03:00
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 ) ;
}
2025-01-09 18:07:02 +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
2025-01-09 18:07:02 +02:00
eu . route ( router , "get" , "/etapi/notes/:noteId/content" , ( req , res , next ) = > {
2022-01-10 17:09:20 +01:00
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 ) ;
2025-01-09 18:07:02 +02:00
res . setHeader ( "Content-Disposition" , utils . getContentDisposition ( filename ) ) ;
2022-01-08 12:01:54 +01:00
res . setHeader ( "Cache-Control" , "no-cache, no-store, must-revalidate" ) ;
2025-01-09 18:07:02 +02:00
res . setHeader ( "Content-Type" , note . mime ) ;
2022-01-08 12:01:54 +01:00
res . send ( note . getContent ( ) ) ;
} ) ;
2025-01-09 18:07:02 +02:00
eu . route ( router , "put" , "/etapi/notes/:noteId/content" , ( req , res , next ) = > {
2022-01-10 17:09:20 +01:00
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
2025-01-09 18:07:02 +02: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
}
2025-01-09 18:07:02 +02:00
const taskContext = new TaskContext ( "no-progress-reporting" ) ;
2022-07-24 21:30:29 +02:00
// 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
2025-01-09 18:07:02 +02: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 ) ;
2025-01-09 18:07:02 +02:00
const taskContext = new TaskContext ( "no-progress-reporting" ) ;
2023-06-15 23:21:40 +02:00
2025-01-09 18:07:02 +02:00
zipImportService . importZip ( taskContext , req . body , note ) . then ( ( importedNote ) = > {
2023-06-15 23:21:40 +02:00
res . status ( 201 ) . json ( {
note : mappers.mapNoteToPojo ( importedNote ) ,
2025-01-09 18:07:02 +02: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.
} ) ;
2025-01-09 18:07:02 +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
2025-01-09 18:07:02 +02:00
eu . route ( router , "get" , "/etapi/notes/:noteId/attachments" , ( req , res , next ) = > {
2023-06-05 09:23:42 +02:00
const note = eu . getAndCheckNote ( req . params . noteId ) ;
2025-01-09 18:07:02 +02:00
const attachments = note . getAttachments ( { includeContentLength : true } ) ;
2023-06-05 09:23:42 +02:00
2025-01-09 18:07:02 +02:00
res . json ( attachments . map ( ( attachment ) = > mappers . mapAttachmentToPojo ( attachment ) ) ) ;
2023-06-05 09:23:42 +02:00
} ) ;
2022-01-07 19:33:59 +01:00
}
2024-12-10 22:35:23 +02:00
function parseSearchParams ( req : Request ) {
2024-04-07 16:56:45 +03:00
const rawSearchParams : SearchParams = {
2025-01-09 18:07:02 +02:00
fastSearch : parseBoolean ( req . query , "fastSearch" ) ,
includeArchivedNotes : parseBoolean ( req . query , "includeArchivedNotes" ) ,
ancestorNoteId : parseString ( req . query [ "ancestorNoteId" ] ) ,
ancestorDepth : parseString ( req . query [ "ancestorDepth" ] ) , // e.g. "eq5"
orderBy : parseString ( req . query [ "orderBy" ] ) ,
2024-04-07 16:56:45 +03:00
// TODO: Check why the order direction was provided as a number, but it's a string everywhere else.
2025-01-09 18:07:02 +02:00
orderDirection : parseOrderDirection ( req . query , "orderDirection" ) as unknown as string ,
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 ;
}
2025-01-09 18:07:02 +02:00
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
}
2025-01-09 18:07:02 +02:00
return obj [ name ] === "true" ;
2022-01-08 13:18:12 +01:00
}
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 ] ) ;
2025-01-09 18:07:02 +02:00
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-07-18 21:42:44 +03:00
export default {
2022-01-07 19:33:59 +01:00
register
2022-01-07 23:06:04 +01:00
} ;