@@ -95,6 +95,8 @@ import * as odataResponse from './odata-response';
9595import { env } from '../server-glue/module' ;
9696import { translateAbstractSqlModel } from './translations' ;
9797
98+ const validBatchMethods = new Set ( [ 'PUT' , 'POST' , 'PATCH' , 'DELETE' , 'GET' ] ) ;
99+
98100const LF2AbstractSQLTranslator = LF2AbstractSQL . createTranslator ( sbvrTypes ) ;
99101const LF2AbstractSQLTranslatorVersion = `${ LF2AbstractSQLVersion } +${ sbvrTypesVersion } ` ;
100102
@@ -1109,6 +1111,7 @@ const $getAffectedIds = async ({
11091111 const parsedRequest : uriParser . ParsedODataRequest &
11101112 Partial < Pick < uriParser . ODataRequest , 'engine' | 'translateVersions' > > =
11111113 await uriParser . parseOData ( {
1114+ id : request . id ,
11121115 method : request . method ,
11131116 url : `/${ request . vocabulary } ${ request . url } ` ,
11141117 } ) ;
@@ -1154,11 +1157,101 @@ const $getAffectedIds = async ({
11541157 return result . rows . map ( ( row ) => row [ idField ] ) ;
11551158} ;
11561159
1160+ const validateBatch = ( req : Express . Request ) => {
1161+ const { requests } = req . body as { requests : uriParser . UnparsedRequest [ ] } ;
1162+ if ( ! Array . isArray ( requests ) ) {
1163+ throw new BadRequestError (
1164+ 'Batch requests must include an array of requests in the body via the "requests" property' ,
1165+ ) ;
1166+ }
1167+ if ( req . headers != null && req . headers [ 'content-type' ] == null ) {
1168+ throw new BadRequestError (
1169+ 'Headers in a batch request must include a "content-type" header if they are provided' ,
1170+ ) ;
1171+ }
1172+ if (
1173+ requests . find (
1174+ ( request ) =>
1175+ request . headers ?. authorization != null ||
1176+ request . url ?. includes ( 'apikey=' ) ,
1177+ ) != null
1178+ ) {
1179+ throw new BadRequestError (
1180+ 'Authorization may only be passed to the main batch request' ,
1181+ ) ;
1182+ }
1183+ const ids = new Set < string > (
1184+ requests
1185+ . map ( ( request ) => request . id )
1186+ . filter ( ( id ) => typeof id === 'string' ) as string [ ] ,
1187+ ) ;
1188+ if ( ids . size !== requests . length ) {
1189+ throw new BadRequestError (
1190+ 'All requests in a batch request must have unique string ids' ,
1191+ ) ;
1192+ }
1193+
1194+ for ( const request of requests ) {
1195+ if (
1196+ request . headers != null &&
1197+ request . headers [ 'content-type' ] == null &&
1198+ ( req . headers == null || req . headers [ 'content-type' ] == null )
1199+ ) {
1200+ throw new BadRequestError (
1201+ 'Requests of a batch request that have headers must include a "content-type" header' ,
1202+ ) ;
1203+ }
1204+ if ( request . method == null ) {
1205+ throw new BadRequestError (
1206+ 'Requests of a batch request must have a "method"' ,
1207+ ) ;
1208+ }
1209+ const upperCaseMethod = request . method . toUpperCase ( ) ;
1210+ if ( ! validBatchMethods . has ( upperCaseMethod ) ) {
1211+ throw new BadRequestError (
1212+ `Requests of a batch request must have a method matching one of the following: ${ Array . from (
1213+ validBatchMethods ,
1214+ ) . join ( ', ' ) } `,
1215+ ) ;
1216+ }
1217+ if (
1218+ request . body !== undefined &&
1219+ ( upperCaseMethod === 'GET' || upperCaseMethod === 'DELETE' )
1220+ ) {
1221+ throw new BadRequestError (
1222+ 'GET and DELETE requests of a batch request must not have a body' ,
1223+ ) ;
1224+ }
1225+ }
1226+
1227+ const urls = new Set < string | undefined > (
1228+ requests . map ( ( request ) => request . url ) ,
1229+ ) ;
1230+ if ( urls . has ( undefined ) ) {
1231+ throw new BadRequestError ( 'Requests of a batch request must have a "url"' ) ;
1232+ }
1233+ if ( urls . has ( '/university/$batch' ) ) {
1234+ throw new BadRequestError ( 'Batch requests cannot contain batch requests' ) ;
1235+ }
1236+ const urlModels = new Set (
1237+ Array . from ( urls . values ( ) ) . map ( ( url : string ) => url . split ( '/' ) [ 1 ] ) ,
1238+ ) ;
1239+ if ( urlModels . size > 1 ) {
1240+ throw new BadRequestError (
1241+ 'Batch requests must consist of requests for only one model' ,
1242+ ) ;
1243+ }
1244+ } ;
1245+
11571246const runODataRequest = ( req : Express . Request , vocabulary : string ) => {
11581247 if ( env . DEBUG ) {
11591248 api [ vocabulary ] . logger . log ( 'Parsing' , req . method , req . url ) ;
11601249 }
11611250
1251+ if ( req . url . startsWith ( `/${ vocabulary } /$batch` ) ) {
1252+ validateBatch ( req ) ;
1253+ }
1254+
11621255 // Get the hooks for the current method/vocabulary as we know it,
11631256 // in order to run PREPARSE hooks, before parsing gets us more info
11641257 const { versions } = models [ vocabulary ] ;
@@ -1206,11 +1299,20 @@ const runODataRequest = (req: Express.Request, vocabulary: string) => {
12061299 await runHooks ( 'PREPARSE' , reqHooks , { req, tx : req . tx } ) ;
12071300 let requests : uriParser . UnparsedRequest [ ] ;
12081301 // Check if it is a single request or a batch
1209- if ( req . batch != null && req . batch . length > 0 ) {
1210- requests = req . batch ;
1302+ if ( req . url . startsWith ( `/${ vocabulary } /$batch` ) ) {
1303+ await Promise . all (
1304+ req . body . requests . map (
1305+ async ( request : HookReq ) =>
1306+ await runHooks ( 'PREPARSE' , reqHooks , {
1307+ req : request ,
1308+ tx : req . tx ,
1309+ } ) ,
1310+ ) ,
1311+ ) ;
1312+ requests = req . body . requests ;
12111313 } else {
12121314 const { method, url, body } = req ;
1213- requests = [ { method, url, data : body } ] ;
1315+ requests = [ { method, url, body } ] ;
12141316 }
12151317
12161318 const prepareRequest = async (
@@ -1274,7 +1376,13 @@ const runODataRequest = (req: Express.Request, vocabulary: string) => {
12741376
12751377 // Parse the OData requests
12761378 const results = await mappingFn ( requests , async ( requestPart ) => {
1277- const parsedRequest = await uriParser . parseOData ( requestPart ) ;
1379+ const parsedRequest = await uriParser . parseOData (
1380+ requestPart ,
1381+ req . url . startsWith ( `/${ vocabulary } /$batch` ) &&
1382+ ! requestPart . url . includes ( `/${ vocabulary } /$batch` )
1383+ ? req . headers
1384+ : undefined ,
1385+ ) ;
12781386
12791387 let request : uriParser . ODataRequest | uriParser . ODataRequest [ ] ;
12801388 if ( Array . isArray ( parsedRequest ) ) {
@@ -1349,7 +1457,10 @@ export const handleODataRequest: Express.Handler = async (req, res, next) => {
13491457
13501458 res . set ( 'Cache-Control' , 'no-cache' ) ;
13511459 // If we are dealing with a single request unpack the response and respond normally
1352- if ( req . batch == null || req . batch . length === 0 ) {
1460+ if (
1461+ ! req . url . startsWith ( `/${ apiRoot } /$batch` ) ||
1462+ req . body . requests ?. length === 0
1463+ ) {
13531464 let [ response ] = responses ;
13541465 if ( response instanceof HttpError ) {
13551466 response = httpErrorToResponse ( response ) ;
@@ -1358,15 +1469,15 @@ export const handleODataRequest: Express.Handler = async (req, res, next) => {
13581469
13591470 // Otherwise its a multipart request and we reply with the appropriate multipart response
13601471 } else {
1361- ( res . status ( 200 ) as any ) . sendMulti (
1362- responses . map ( ( response ) => {
1472+ res . status ( 200 ) . json ( {
1473+ responses : responses . map ( ( response ) => {
13631474 if ( response instanceof HttpError ) {
13641475 return httpErrorToResponse ( response ) ;
13651476 } else {
13661477 return response ;
13671478 }
13681479 } ) ,
1369- ) ;
1480+ } ) ;
13701481 }
13711482 } catch ( e : any ) {
13721483 if ( handleHttpErrors ( req , res , e ) ) {
@@ -1393,7 +1504,7 @@ export const handleHttpErrors = (
13931504 for ( const handleErrorFn of handleErrorFns ) {
13941505 handleErrorFn ( req , err ) ;
13951506 }
1396- const response = httpErrorToResponse ( err ) ;
1507+ const response = httpErrorToResponse ( err , req ) ;
13971508 handleResponse ( res , response ) ;
13981509 return true ;
13991510 }
@@ -1412,10 +1523,12 @@ const handleResponse = (res: Express.Response, response: Response): void => {
14121523
14131524const httpErrorToResponse = (
14141525 err : HttpError ,
1526+ req ?: Express . Request ,
14151527) : RequiredField < Response , 'status' > => {
1528+ const message = err . getResponseBody ( ) ;
14161529 return {
14171530 status : err . status ,
1418- body : err . getResponseBody ( ) ,
1531+ body : req != null && 'batch' in req ? { responses : [ ] , message } : message ,
14191532 headers : err . headers ,
14201533 } ;
14211534} ;
0 commit comments