Skip to content

Commit b3386e8

Browse files
committed
$batch
Change-type: major
1 parent 77dfb77 commit b3386e8

File tree

9 files changed

+541
-30
lines changed

9 files changed

+541
-30
lines changed

src/sbvr-api/sbvr-utils.ts

Lines changed: 77 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,8 @@ export interface ApiKey extends Actor {
133133
}
134134

135135
export interface Response {
136-
statusCode: number;
136+
id?: string | undefined;
137+
status: number;
137138
headers?:
138139
| {
139140
[headerName: string]: any;
@@ -1022,15 +1023,15 @@ export const runURI = async (
10221023
throw response;
10231024
}
10241025

1025-
const { body: responseBody, statusCode, headers } = response as Response;
1026+
const { body: responseBody, status, headers } = response as Response;
10261027

1027-
if (statusCode != null && statusCode >= 400) {
1028+
if (status != null && status >= 400) {
10281029
const ErrorClass =
1029-
statusCodeToError[statusCode as keyof typeof statusCodeToError];
1030+
statusCodeToError[status as keyof typeof statusCodeToError];
10301031
if (ErrorClass != null) {
10311032
throw new ErrorClass(undefined, responseBody, headers);
10321033
}
1033-
throw new HttpError(statusCode, undefined, responseBody, headers);
1034+
throw new HttpError(status, undefined, responseBody, headers);
10341035
}
10351036

10361037
return responseBody as AnyObject | undefined;
@@ -1069,7 +1070,7 @@ export const getAffectedIds = async (
10691070
args: HookArgs & {
10701071
tx: Db.Tx;
10711072
},
1072-
): Promise<number[]> => {
1073+
): Promise<string[]> => {
10731074
const { request } = args;
10741075
if (request.affectedIds) {
10751076
return request.affectedIds;
@@ -1094,7 +1095,7 @@ const $getAffectedIds = async ({
10941095
tx,
10951096
}: HookArgs & {
10961097
tx: Db.Tx;
1097-
}): Promise<number[]> => {
1098+
}): Promise<string[]> => {
10981099
if (!['PATCH', 'DELETE'].includes(request.method)) {
10991100
// We can only find the affected ids in advance for requests that modify existing records, if they
11001101
// can insert new records (POST/PUT) then we're unable to find the ids until the request has actually run
@@ -1108,6 +1109,7 @@ const $getAffectedIds = async ({
11081109
const parsedRequest: uriParser.ParsedODataRequest &
11091110
Partial<Pick<uriParser.ODataRequest, 'engine' | 'translateVersions'>> =
11101111
await uriParser.parseOData({
1112+
id: request.id,
11111113
method: request.method,
11121114
url: `/${request.vocabulary}${request.url}`,
11131115
});
@@ -1158,6 +1160,44 @@ const runODataRequest = (req: Express.Request, vocabulary: string) => {
11581160
api[vocabulary].logger.log('Parsing', req.method, req.url);
11591161
}
11601162

1163+
if (req.url === `/${vocabulary}/$batch`) {
1164+
const { requests } = req.body as { requests: uriParser.UnparsedRequest[] };
1165+
const ids = new Set<string>(
1166+
requests
1167+
.map((request) => request.id)
1168+
.filter((id) => typeof id === 'string') as string[],
1169+
);
1170+
if (ids.size !== requests.length) {
1171+
throw new BadRequestError(
1172+
'All requests in a batch request must have unique string ids',
1173+
);
1174+
}
1175+
1176+
const methods = new Set<string | undefined>(
1177+
requests.map((request) => request.method),
1178+
);
1179+
if (methods.has(undefined)) {
1180+
throw new BadRequestError(
1181+
'Requests of a batch request must have a "method"',
1182+
);
1183+
}
1184+
1185+
const urls = new Set<string | undefined>(
1186+
requests.map((request) => request.url),
1187+
);
1188+
if (urls.has(undefined)) {
1189+
throw new BadRequestError(
1190+
'Requests of a batch request must have a "url"',
1191+
);
1192+
}
1193+
if (urls.has('/university/$batch')) {
1194+
throw new BadRequestError('Batch requests cannot contain batch requests');
1195+
}
1196+
1197+
// TODO: make sure req.body.requests is valid structure/typing for req.batch
1198+
req.batch = requests;
1199+
}
1200+
11611201
// Get the hooks for the current method/vocabulary as we know it,
11621202
// in order to run PREPARSE hooks, before parsing gets us more info
11631203
const { versions } = models[vocabulary];
@@ -1205,17 +1245,22 @@ const runODataRequest = (req: Express.Request, vocabulary: string) => {
12051245
await runHooks('PREPARSE', reqHooks, { req, tx: req.tx });
12061246
let requests: uriParser.UnparsedRequest[];
12071247
// Check if it is a single request or a batch
1248+
// console.error('+++++++++++++++++++', req.url, req.batch);
12081249
if (req.batch != null && req.batch.length > 0) {
12091250
requests = req.batch;
12101251
} else {
12111252
const { method, url, body } = req;
1212-
requests = [{ method, url, data: body }];
1253+
requests = [{ method, url, body }];
12131254
}
1255+
// console.error('+++++++++++++++++++', req.url, requests);
12141256

12151257
const prepareRequest = async (
12161258
parsedRequest: uriParser.ParsedODataRequest &
12171259
Partial<Pick<uriParser.ODataRequest, 'engine' | 'translateVersions'>>,
12181260
): Promise<uriParser.ODataRequest> => {
1261+
// if (process.env.something) {
1262+
// console.error('parsedRequest', parsedRequest);
1263+
// }
12191264
parsedRequest.engine = db.engine;
12201265
parsedRequest.translateVersions = [...versions];
12211266
// Mark that the engine/translateVersions is required now that we've set it
@@ -1226,6 +1271,7 @@ const runODataRequest = (req: Express.Request, vocabulary: string) => {
12261271
// Add/check the relevant permissions
12271272
try {
12281273
$request.hooks = [];
1274+
// console.error('a');
12291275
for (const version of versions) {
12301276
// We get the hooks list between each `runHooks` so that any resource renames will be used
12311277
// when getting hooks for later versions
@@ -1247,6 +1293,7 @@ const runODataRequest = (req: Express.Request, vocabulary: string) => {
12471293
request: $request,
12481294
tx: req.tx,
12491295
});
1296+
// console.error('b', version);
12501297
const { resourceRenames } = models[version];
12511298
if (resourceRenames) {
12521299
const resourceName = resolveSynonym($request);
@@ -1257,7 +1304,10 @@ const runODataRequest = (req: Express.Request, vocabulary: string) => {
12571304
}
12581305
}
12591306
}
1307+
// console.error('$request', $request.id);
12601308
const translatedRequest = await uriParser.translateUri($request);
1309+
// console.error('translatedRequest', translatedRequest.id);
1310+
// console.error('c');
12611311
return await compileRequest(translatedRequest);
12621312
} catch (err: any) {
12631313
rollbackRequestHooks(reqHooks);
@@ -1266,17 +1316,18 @@ const runODataRequest = (req: Express.Request, vocabulary: string) => {
12661316
}
12671317
};
12681318

1319+
// console.error('1');
12691320
// Parse the OData requests
12701321
const results = await mappingFn(requests, async (requestPart) => {
12711322
const parsedRequest = await uriParser.parseOData(requestPart);
1323+
// console.error('2');
12721324

12731325
let request: uriParser.ODataRequest | uriParser.ODataRequest[];
12741326
if (Array.isArray(parsedRequest)) {
12751327
request = await controlFlow.mapSeries(parsedRequest, prepareRequest);
12761328
} else {
12771329
request = await prepareRequest(parsedRequest);
12781330
}
1279-
// Run the request in its own transaction
12801331
return await runTransaction<Response | Response[]>(
12811332
req,
12821333
request,
@@ -1293,7 +1344,7 @@ const runODataRequest = (req: Express.Request, vocabulary: string) => {
12931344
}
12941345
});
12951346
if (Array.isArray(request)) {
1296-
const changeSetResults = new Map<number, Response>();
1347+
const changeSetResults = new Map<string, Response>();
12971348
const changeSetRunner = runChangeSet(req, tx);
12981349
for (const r of request) {
12991350
await changeSetRunner(changeSetResults, r);
@@ -1314,7 +1365,7 @@ const runODataRequest = (req: Express.Request, vocabulary: string) => {
13141365
if (
13151366
!Array.isArray(result) &&
13161367
result.body == null &&
1317-
result.statusCode == null
1368+
result.status == null
13181369
) {
13191370
console.error('No status or body set', req.url, responses);
13201371
return new InternalRequestError();
@@ -1352,10 +1403,10 @@ export const handleODataRequest: Express.Handler = async (req, res, next) => {
13521403

13531404
// Otherwise its a multipart request and we reply with the appropriate multipart response
13541405
} else {
1355-
(res.status(200) as any).sendMulti(
1406+
res.status(200).json(
13561407
responses.map((response) => {
13571408
if (response instanceof HttpError) {
1358-
response = httpErrorToResponse(response);
1409+
return httpErrorToResponse(response);
13591410
} else {
13601411
return response;
13611412
}
@@ -1394,9 +1445,9 @@ export const handleHttpErrors = (
13941445
return false;
13951446
};
13961447
const handleResponse = (res: Express.Response, response: Response): void => {
1397-
const { body, headers, statusCode } = response as Response;
1448+
const { body, headers, status } = response as Response;
13981449
res.set(headers);
1399-
res.status(statusCode);
1450+
res.status(status);
14001451
if (!body) {
14011452
res.end();
14021453
} else {
@@ -1406,9 +1457,9 @@ const handleResponse = (res: Express.Response, response: Response): void => {
14061457

14071458
const httpErrorToResponse = (
14081459
err: HttpError,
1409-
): RequiredField<Response, 'statusCode'> => {
1460+
): RequiredField<Response, 'status'> => {
14101461
return {
1411-
statusCode: err.status,
1462+
status: err.status,
14121463
body: err.getResponseBody(),
14131464
headers: err.headers,
14141465
};
@@ -1514,7 +1565,7 @@ const runRequest = async (
15141565
const runChangeSet =
15151566
(req: Express.Request, tx: Db.Tx) =>
15161567
async (
1517-
changeSetResults: Map<number, Response>,
1568+
changeSetResults: Map<string, Response>,
15181569
request: uriParser.ODataRequest,
15191570
): Promise<void> => {
15201571
request = updateBinds(changeSetResults, request);
@@ -1532,7 +1583,7 @@ const runChangeSet =
15321583
// deferred untill the request they reference is run and returns an insert ID.
15331584
// This function compiles the sql query of a request which has been deferred
15341585
const updateBinds = (
1535-
changeSetResults: Map<number, Response>,
1586+
changeSetResults: Map<string, Response>,
15361587
request: uriParser.ODataRequest,
15371588
) => {
15381589
if (request._defer) {
@@ -1700,7 +1751,8 @@ const respondGet = async (
17001751
);
17011752

17021753
const response = {
1703-
statusCode: 200,
1754+
id: request.id,
1755+
status: 200,
17041756
body: { d },
17051757
headers: { 'content-type': 'application/json' },
17061758
};
@@ -1715,14 +1767,15 @@ const respondGet = async (
17151767
} else {
17161768
if (request.resourceName === '$metadata') {
17171769
return {
1718-
statusCode: 200,
1770+
id: request.id,
1771+
status: 200,
17191772
body: models[vocab].odataMetadata,
17201773
headers: { 'content-type': 'xml' },
17211774
};
17221775
} else {
17231776
// TODO: request.resourceName can be '$serviceroot' or a resource and we should return an odata xml document based on that
17241777
return {
1725-
statusCode: 404,
1778+
status: 404,
17261779
};
17271780
}
17281781
}
@@ -1778,7 +1831,7 @@ const respondPost = async (
17781831
}
17791832

17801833
const response = {
1781-
statusCode: 201,
1834+
status: 201,
17821835
body: result.d[0],
17831836
headers: {
17841837
'content-type': 'application/json',
@@ -1826,7 +1879,7 @@ const respondPut = async (
18261879
tx: Db.Tx,
18271880
): Promise<Response> => {
18281881
const response = {
1829-
statusCode: 200,
1882+
status: 200,
18301883
};
18311884
await runHooks('PRERESPOND', request.hooks, {
18321885
req,

src/sbvr-api/uri-parser.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ import * as sbvrUtils from './sbvr-utils';
2929
export type OdataBinds = ODataBinds;
3030

3131
export interface UnparsedRequest {
32+
id?: string;
3233
method: string;
3334
url: string;
34-
data?: any;
35+
body?: any;
3536
headers?: { [header: string]: string };
3637
changeSet?: UnparsedRequest[];
3738
_isChangeSet?: boolean;
@@ -47,7 +48,7 @@ export interface ParsedODataRequest {
4748
odataQuery: ODataQuery;
4849
odataBinds: OdataBinds;
4950
custom: AnyObject;
50-
id?: number | undefined;
51+
id?: string | undefined;
5152
_defer?: boolean;
5253
}
5354
export interface ODataRequest extends ParsedODataRequest {
@@ -60,8 +61,8 @@ export interface ODataRequest extends ParsedODataRequest {
6061
modifiedFields?: ReturnType<
6162
AbstractSQLCompiler.EngineInstance['getModifiedFields']
6263
>;
63-
affectedIds?: number[];
64-
pendingAffectedIds?: Promise<number[]>;
64+
affectedIds?: string[];
65+
pendingAffectedIds?: Promise<string[]>;
6566
hooks?: Array<[string, InstantiatedHooks]>;
6667
engine: AbstractSQLCompiler.Engines;
6768
}
@@ -292,12 +293,13 @@ export async function parseOData(
292293
const odata = memoizedParseOdata(url);
293294

294295
return {
296+
id: b.id,
295297
method: b.method as SupportedMethod,
296298
url,
297299
vocabulary: apiRoot,
298300
resourceName: odata.tree.resource,
299301
originalResourceName: odata.tree.resource,
300-
values: b.data ?? {},
302+
values: b.body ?? {},
301303
odataQuery: odata.tree,
302304
odataBinds: odata.binds,
303305
custom: {},
@@ -362,7 +364,7 @@ const parseODataChangeset = (
362364
originalResourceName: odata.tree.resource,
363365
odataBinds: odata.binds,
364366
odataQuery: odata.tree,
365-
values: b.data ?? {},
367+
values: b.body ?? {},
366368
custom: {},
367369
id: contentId,
368370
_defer: defer,

0 commit comments

Comments
 (0)