Skip to content

Commit

Permalink
$batch
Browse files Browse the repository at this point in the history
Change-type: major
  • Loading branch information
myarmolinsky committed Jun 30, 2023
1 parent 77dfb77 commit b3386e8
Show file tree
Hide file tree
Showing 9 changed files with 541 additions and 30 deletions.
101 changes: 77 additions & 24 deletions src/sbvr-api/sbvr-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,8 @@ export interface ApiKey extends Actor {
}

export interface Response {
statusCode: number;
id?: string | undefined;
status: number;
headers?:
| {
[headerName: string]: any;
Expand Down Expand Up @@ -1022,15 +1023,15 @@ export const runURI = async (
throw response;
}

const { body: responseBody, statusCode, headers } = response as Response;
const { body: responseBody, status, headers } = response as Response;

if (statusCode != null && statusCode >= 400) {
if (status != null && status >= 400) {
const ErrorClass =
statusCodeToError[statusCode as keyof typeof statusCodeToError];
statusCodeToError[status as keyof typeof statusCodeToError];
if (ErrorClass != null) {
throw new ErrorClass(undefined, responseBody, headers);
}
throw new HttpError(statusCode, undefined, responseBody, headers);
throw new HttpError(status, undefined, responseBody, headers);
}

return responseBody as AnyObject | undefined;
Expand Down Expand Up @@ -1069,7 +1070,7 @@ export const getAffectedIds = async (
args: HookArgs & {
tx: Db.Tx;
},
): Promise<number[]> => {
): Promise<string[]> => {
const { request } = args;
if (request.affectedIds) {
return request.affectedIds;
Expand All @@ -1094,7 +1095,7 @@ const $getAffectedIds = async ({
tx,
}: HookArgs & {
tx: Db.Tx;
}): Promise<number[]> => {
}): Promise<string[]> => {
if (!['PATCH', 'DELETE'].includes(request.method)) {
// We can only find the affected ids in advance for requests that modify existing records, if they
// can insert new records (POST/PUT) then we're unable to find the ids until the request has actually run
Expand All @@ -1108,6 +1109,7 @@ const $getAffectedIds = async ({
const parsedRequest: uriParser.ParsedODataRequest &
Partial<Pick<uriParser.ODataRequest, 'engine' | 'translateVersions'>> =
await uriParser.parseOData({
id: request.id,
method: request.method,
url: `/${request.vocabulary}${request.url}`,
});
Expand Down Expand Up @@ -1158,6 +1160,44 @@ const runODataRequest = (req: Express.Request, vocabulary: string) => {
api[vocabulary].logger.log('Parsing', req.method, req.url);
}

if (req.url === `/${vocabulary}/$batch`) {
const { requests } = req.body as { requests: uriParser.UnparsedRequest[] };
const ids = new Set<string>(
requests
.map((request) => request.id)
.filter((id) => typeof id === 'string') as string[],
);
if (ids.size !== requests.length) {
throw new BadRequestError(
'All requests in a batch request must have unique string ids',
);
}

const methods = new Set<string | undefined>(
requests.map((request) => request.method),
);
if (methods.has(undefined)) {
throw new BadRequestError(
'Requests of a batch request must have a "method"',
);
}

const urls = new Set<string | undefined>(
requests.map((request) => request.url),
);
if (urls.has(undefined)) {
throw new BadRequestError(
'Requests of a batch request must have a "url"',
);
}
if (urls.has('/university/$batch')) {
throw new BadRequestError('Batch requests cannot contain batch requests');
}

// TODO: make sure req.body.requests is valid structure/typing for req.batch
req.batch = requests;
}

// Get the hooks for the current method/vocabulary as we know it,
// in order to run PREPARSE hooks, before parsing gets us more info
const { versions } = models[vocabulary];
Expand Down Expand Up @@ -1205,17 +1245,22 @@ const runODataRequest = (req: Express.Request, vocabulary: string) => {
await runHooks('PREPARSE', reqHooks, { req, tx: req.tx });
let requests: uriParser.UnparsedRequest[];
// Check if it is a single request or a batch
// console.error('+++++++++++++++++++', req.url, req.batch);
if (req.batch != null && req.batch.length > 0) {
requests = req.batch;
} else {
const { method, url, body } = req;
requests = [{ method, url, data: body }];
requests = [{ method, url, body }];
}
// console.error('+++++++++++++++++++', req.url, requests);

const prepareRequest = async (
parsedRequest: uriParser.ParsedODataRequest &
Partial<Pick<uriParser.ODataRequest, 'engine' | 'translateVersions'>>,
): Promise<uriParser.ODataRequest> => {
// if (process.env.something) {
// console.error('parsedRequest', parsedRequest);
// }
parsedRequest.engine = db.engine;
parsedRequest.translateVersions = [...versions];
// Mark that the engine/translateVersions is required now that we've set it
Expand All @@ -1226,6 +1271,7 @@ const runODataRequest = (req: Express.Request, vocabulary: string) => {
// Add/check the relevant permissions
try {
$request.hooks = [];
// console.error('a');
for (const version of versions) {
// We get the hooks list between each `runHooks` so that any resource renames will be used
// when getting hooks for later versions
Expand All @@ -1247,6 +1293,7 @@ const runODataRequest = (req: Express.Request, vocabulary: string) => {
request: $request,
tx: req.tx,
});
// console.error('b', version);
const { resourceRenames } = models[version];
if (resourceRenames) {
const resourceName = resolveSynonym($request);
Expand All @@ -1257,7 +1304,10 @@ const runODataRequest = (req: Express.Request, vocabulary: string) => {
}
}
}
// console.error('$request', $request.id);
const translatedRequest = await uriParser.translateUri($request);
// console.error('translatedRequest', translatedRequest.id);
// console.error('c');
return await compileRequest(translatedRequest);
} catch (err: any) {
rollbackRequestHooks(reqHooks);
Expand All @@ -1266,17 +1316,18 @@ const runODataRequest = (req: Express.Request, vocabulary: string) => {
}
};

// console.error('1');
// Parse the OData requests
const results = await mappingFn(requests, async (requestPart) => {
const parsedRequest = await uriParser.parseOData(requestPart);
// console.error('2');

let request: uriParser.ODataRequest | uriParser.ODataRequest[];
if (Array.isArray(parsedRequest)) {
request = await controlFlow.mapSeries(parsedRequest, prepareRequest);
} else {
request = await prepareRequest(parsedRequest);
}
// Run the request in its own transaction
return await runTransaction<Response | Response[]>(
req,
request,
Expand All @@ -1293,7 +1344,7 @@ const runODataRequest = (req: Express.Request, vocabulary: string) => {
}
});
if (Array.isArray(request)) {
const changeSetResults = new Map<number, Response>();
const changeSetResults = new Map<string, Response>();
const changeSetRunner = runChangeSet(req, tx);
for (const r of request) {
await changeSetRunner(changeSetResults, r);
Expand All @@ -1314,7 +1365,7 @@ const runODataRequest = (req: Express.Request, vocabulary: string) => {
if (
!Array.isArray(result) &&
result.body == null &&
result.statusCode == null
result.status == null
) {
console.error('No status or body set', req.url, responses);
return new InternalRequestError();
Expand Down Expand Up @@ -1352,10 +1403,10 @@ export const handleODataRequest: Express.Handler = async (req, res, next) => {

// Otherwise its a multipart request and we reply with the appropriate multipart response
} else {
(res.status(200) as any).sendMulti(
res.status(200).json(
responses.map((response) => {
if (response instanceof HttpError) {
response = httpErrorToResponse(response);
return httpErrorToResponse(response);
} else {
return response;
}
Expand Down Expand Up @@ -1394,9 +1445,9 @@ export const handleHttpErrors = (
return false;
};
const handleResponse = (res: Express.Response, response: Response): void => {
const { body, headers, statusCode } = response as Response;
const { body, headers, status } = response as Response;
res.set(headers);
res.status(statusCode);
res.status(status);
if (!body) {
res.end();
} else {
Expand All @@ -1406,9 +1457,9 @@ const handleResponse = (res: Express.Response, response: Response): void => {

const httpErrorToResponse = (
err: HttpError,
): RequiredField<Response, 'statusCode'> => {
): RequiredField<Response, 'status'> => {
return {
statusCode: err.status,
status: err.status,
body: err.getResponseBody(),
headers: err.headers,
};
Expand Down Expand Up @@ -1514,7 +1565,7 @@ const runRequest = async (
const runChangeSet =
(req: Express.Request, tx: Db.Tx) =>
async (
changeSetResults: Map<number, Response>,
changeSetResults: Map<string, Response>,
request: uriParser.ODataRequest,
): Promise<void> => {
request = updateBinds(changeSetResults, request);
Expand All @@ -1532,7 +1583,7 @@ const runChangeSet =
// deferred untill the request they reference is run and returns an insert ID.
// This function compiles the sql query of a request which has been deferred
const updateBinds = (
changeSetResults: Map<number, Response>,
changeSetResults: Map<string, Response>,
request: uriParser.ODataRequest,
) => {
if (request._defer) {
Expand Down Expand Up @@ -1700,7 +1751,8 @@ const respondGet = async (
);

const response = {
statusCode: 200,
id: request.id,
status: 200,
body: { d },
headers: { 'content-type': 'application/json' },
};
Expand All @@ -1715,14 +1767,15 @@ const respondGet = async (
} else {
if (request.resourceName === '$metadata') {
return {
statusCode: 200,
id: request.id,
status: 200,
body: models[vocab].odataMetadata,
headers: { 'content-type': 'xml' },
};
} else {
// TODO: request.resourceName can be '$serviceroot' or a resource and we should return an odata xml document based on that
return {
statusCode: 404,
status: 404,
};
}
}
Expand Down Expand Up @@ -1778,7 +1831,7 @@ const respondPost = async (
}

const response = {
statusCode: 201,
status: 201,
body: result.d[0],
headers: {
'content-type': 'application/json',
Expand Down Expand Up @@ -1826,7 +1879,7 @@ const respondPut = async (
tx: Db.Tx,
): Promise<Response> => {
const response = {
statusCode: 200,
status: 200,
};
await runHooks('PRERESPOND', request.hooks, {
req,
Expand Down
14 changes: 8 additions & 6 deletions src/sbvr-api/uri-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ import * as sbvrUtils from './sbvr-utils';
export type OdataBinds = ODataBinds;

export interface UnparsedRequest {
id?: string;
method: string;
url: string;
data?: any;
body?: any;
headers?: { [header: string]: string };
changeSet?: UnparsedRequest[];
_isChangeSet?: boolean;
Expand All @@ -47,7 +48,7 @@ export interface ParsedODataRequest {
odataQuery: ODataQuery;
odataBinds: OdataBinds;
custom: AnyObject;
id?: number | undefined;
id?: string | undefined;
_defer?: boolean;
}
export interface ODataRequest extends ParsedODataRequest {
Expand All @@ -60,8 +61,8 @@ export interface ODataRequest extends ParsedODataRequest {
modifiedFields?: ReturnType<
AbstractSQLCompiler.EngineInstance['getModifiedFields']
>;
affectedIds?: number[];
pendingAffectedIds?: Promise<number[]>;
affectedIds?: string[];
pendingAffectedIds?: Promise<string[]>;
hooks?: Array<[string, InstantiatedHooks]>;
engine: AbstractSQLCompiler.Engines;
}
Expand Down Expand Up @@ -292,12 +293,13 @@ export async function parseOData(
const odata = memoizedParseOdata(url);

return {
id: b.id,
method: b.method as SupportedMethod,
url,
vocabulary: apiRoot,
resourceName: odata.tree.resource,
originalResourceName: odata.tree.resource,
values: b.data ?? {},
values: b.body ?? {},
odataQuery: odata.tree,
odataBinds: odata.binds,
custom: {},
Expand Down Expand Up @@ -362,7 +364,7 @@ const parseODataChangeset = (
originalResourceName: odata.tree.resource,
odataBinds: odata.binds,
odataQuery: odata.tree,
values: b.data ?? {},
values: b.body ?? {},
custom: {},
id: contentId,
_defer: defer,
Expand Down
Loading

0 comments on commit b3386e8

Please sign in to comment.