Skip to content

Commit

Permalink
FEATURE - handle changeset scenarios for 412 warnings (#829)
Browse files Browse the repository at this point in the history
* feat: handle changeset scenarios for 412 warnings

changes made to group  412 responses for  batch reqquest in changeset mode  with a single 412 error response
no changes required for isolated mode

* feat: introduce a basic support for etags (#828)

* feat: introduce a basic support for etags

* chore: fix tests

* chore: fix sonar

* Update packages/ui5-middleware-fe-mockserver/src/configResolver.ts

Co-authored-by: Dirk Teufel <50441133+drktfl@users.noreply.github.com>

---------

Co-authored-by: Dirk Teufel <50441133+drktfl@users.noreply.github.com>

* chore: apply latest changesets

* patch bump

* lint fixes

* resolve comments

---------

Co-authored-by: I569023 <abhishek.anand15@sap.com>
Co-authored-by: Nicolas Lunet <nicolas.lunet@sap.com>
Co-authored-by: Dirk Teufel <50441133+drktfl@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
  • Loading branch information
5 people committed Jul 8, 2024
1 parent ec7d972 commit 09d5a17
Show file tree
Hide file tree
Showing 7 changed files with 570 additions and 211 deletions.
5 changes: 5 additions & 0 deletions .changeset/itchy-peas-cry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sap-ux/fe-mockserver-core': patch
---

handle changeset scenarios for 412 warnings
143 changes: 136 additions & 7 deletions packages/fe-mockserver-core/src/router/batchRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,99 @@ import type { Batch, BatchPart } from './batchParser';
import { BatchContent, getBoundary, parseBatch } from './batchParser';
import type { IncomingMessageWithTenant } from './serviceRouter';

type Error412 = Record<string, ErrorResponse>;

type ErrorResponse = {
code: string;
message: string;
severity?: string;
details: ErrorDetails[];
} & Record<string, unknown>;

type ErrorDetails = {
code: string;
message: string;
severity?: string;
} & Record<string, unknown>;

/**
* Returns the result whether isRequest part of changeset or not.
* @param part
* @returns {boolean}
*/
export function isPartChangeSet(part: BatchPart | Batch): part is Batch {
return (part as Batch).isChangeSet;
}

let aggregate412BatchResponseInstance: {
add412Response: (batchPartRes: string, header: string, resContent: Error412, contentId: string | undefined) => void;
getUnifiedResponse: () => string | undefined;
};
const NL = '\r\n';

/**
* Handles the part request.
* @param partDefinition
* @param dataAccess
* @param boundary
* @param tenantId
* @param globalHeaders
* @param isChangeSetPart
* @returns {string | null}
*/
async function handlePart(
partDefinition: BatchPart,
dataAccess: DataAccess,
boundary: string,
tenantId: string,
globalHeaders: Record<string, string>
): Promise<string> {
globalHeaders: Record<string, string>,
isChangeSetPart?: boolean
): Promise<string | null> {
const partRequest = new ODataRequest({ ...partDefinition, tenantId: tenantId }, dataAccess);
await partRequest.handleRequest();
const isResponse412ChangeSet = partRequest?.statusCode === 412 && !!isChangeSetPart;
const { batchPartRes, header, resContent, contentId } = createBatchResponseObject(
partRequest,
partDefinition,
boundary,
globalHeaders,
isResponse412ChangeSet
);
// All 412 batch responses should be transformed and returned as single response
if (isResponse412ChangeSet) {
aggregate412BatchResponseInstance.add412Response(batchPartRes, header, resContent, contentId);
return null;
}
return batchPartRes;
}

/**
* Creates a batch response object.
* @param partRequest
* @param partDefinition
* @param boundary
* @param globalHeaders
* @param isResponse412ChangeSet
* @returns a batch response object
*/
function createBatchResponseObject(
partRequest: ODataRequest,
partDefinition: BatchPart,
boundary: string,
globalHeaders: Record<string, string>,
isResponse412ChangeSet: boolean
) {
let batchResponse = '';
batchResponse += `--${boundary}${NL}`;
batchResponse += `Content-Type: application/http${NL}`;
batchResponse += `Content-Transfer-Encoding: binary${NL}`;
let contentId;
if (partDefinition.contentId) {
batchResponse += `Content-ID: ${partDefinition.contentId}${NL}`;
contentId = partDefinition.contentId;
batchResponse += `Content-ID: ${contentId}${NL}`;
}
if (partRequest.getETag()) {
batchResponse += `ETag: ${partRequest.getETag()}${NL}`;
}
if (partRequest.getETag()) {
batchResponse += `ETag: ${partRequest.getETag()}${NL}`;
Expand All @@ -41,12 +114,61 @@ async function handlePart(
globalHeaders[headerName] = partRequest.globalResponseHeaders[headerName];
}
batchResponse += NL; // End of part header
const header = batchResponse;
if (responseData) {
batchResponse += responseData;
//batchResponse += NL; // End of body content
}
batchResponse += NL;
return batchResponse;
const resContent = isResponse412ChangeSet ? JSON.parse(responseData as string) : null;
return { batchPartRes: batchResponse, header, resContent, contentId };
}

/**
* Creates instance of 412 responses aggregated from batch changeset request.
* @returns void
*/
function aggregate412BatchResponse() {
const batch412Response = {
header: '',
error: {
code: '',
message: '',
severity: '',
details: [] as ErrorDetails[]
} as ErrorResponse
};
let firstPart = true;
return {
add412Response: function (
batchPartRes: string,
header: string,
resContent: Error412,
contentId: string | undefined
) {
if (firstPart) {
batch412Response.header = header;
batch412Response.error = {
code: resContent.error.code,
message: resContent.error.message,
severity: resContent.error['@Common.Severity'] as string | undefined,
details: []
};
firstPart = false;
}
batch412Response.error.details.push(resContent.error.details[0]);
batch412Response.error.details[batch412Response.error.details.length - 1]['Content-ID'] = contentId;
},
getUnifiedResponse: function (): string {
let batchResponse = '';
batchResponse += batch412Response.header;
batchResponse += NL;
const { error } = batch412Response;
batchResponse += JSON.stringify({ error: error });
batchResponse += NL;
return batchResponse;
}
};
}

/**
Expand All @@ -63,21 +185,28 @@ export function batchRouter(dataAccess: DataAccess): NextHandleFunction {
const batchData = parseBatch(new BatchContent(body), boundary);
const globalHeaders: Record<string, string> = {};
let batchResponse = '';

//initialize the instance of aggregator of batch 412 responses
aggregate412BatchResponseInstance = aggregate412BatchResponse();
for (const part of batchData.parts) {
if (isPartChangeSet(part)) {
batchResponse += `--${batchData.boundary}${NL}`;
batchResponse += `Content-Type: multipart/mixed; boundary=${part.boundary}${NL}`;
batchResponse += NL;
for (const changeSetPart of part.parts) {
batchResponse += await handlePart(
const batchPartRes = await handlePart(
changeSetPart as BatchPart,
dataAccess,
part.boundary,
req.tenantId!,
globalHeaders
globalHeaders,
true
);
if (batchPartRes !== null) {
batchResponse += batchPartRes;
}
}
// append the 412 batch response
batchResponse += aggregate412BatchResponseInstance.getUnifiedResponse();
batchResponse += `--${part.boundary}--${NL}`;
} else {
batchResponse += await handlePart(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ exports[`V4 Requestor Sticky create, discard, try to update the discarded item:
exports[`V4 Requestor can create data through a call 1`] = `
{
"@odata.context": "../$metadata#RootElement/$entity",
"@odata.metadataEtag": "W/"60af-tCc45VlNT7csKPRiK0amTPyY35E"",
"@odata.metadataEtag": "W/"6309-CcQCLrzrpArd+m2Ag482j6C0HxA"",
"HasActiveEntity": false,
"HasDraftEntity": false,
"ID": 555,
Expand All @@ -56,7 +56,7 @@ exports[`V4 Requestor can create data through a call 1`] = `
exports[`V4 Requestor can create data through a call 2`] = `
{
"@odata.context": "../$metadata#RootElement/$entity",
"@odata.metadataEtag": "W/"60af-tCc45VlNT7csKPRiK0amTPyY35E"",
"@odata.metadataEtag": "W/"6309-CcQCLrzrpArd+m2Ag482j6C0HxA"",
"HasActiveEntity": false,
"HasDraftEntity": false,
"ID": 666,
Expand Down Expand Up @@ -182,6 +182,10 @@ exports[`V4 Requestor can get the metadata 1`] = `
<Property Name="InProcessByUser" Type="Edm.String" MaxLength="256"/>
<Property Name="DraftIsProcessedByMe" Type="Edm.Boolean"/>
</EntityType>
<Action Name="bound412Action" IsBound="true" EntitySetPath="self">
<Parameter Name="self" Type="sap.fe.core.ActionVisibility.RootElement"/>
<ReturnType Type="sap.fe.core.ActionVisibility.RootElement"/>
</Action>
<Action Name="boundAction1" IsBound="true" EntitySetPath="self">
<Parameter Name="self" Type="sap.fe.core.ActionVisibility.RootElement"/>
<ReturnType Type="sap.fe.core.ActionVisibility.RootElement"/>
Expand Down Expand Up @@ -271,6 +275,11 @@ exports[`V4 Requestor can get the metadata 1`] = `
<PropertyValue Property="Inline" Bool="true"/>
<Annotation Term="UI.Hidden" Path="Sibling/isBoundAction2Hidden"/>
</Record>
<Record Type="UI.DataFieldForAction">
<PropertyValue Property="Label" String="Message - 412 (Bound)"/>
<PropertyValue Property="Action" String="sap.fe.core.ActionVisibility.bound412Action"/>
<PropertyValue Property="InvocationGrouping" EnumMember="UI.OperationGroupingType/ChangeSet"/>
</Record>
</Collection>
</Annotation>
<Annotation Term="UI.HeaderInfo">
Expand Down Expand Up @@ -599,7 +608,7 @@ exports[`V4 Requestor can get the root 1`] = `
exports[`V4 Requestor can get the root 2`] = `
{
"@odata.context": "$metadata",
"@odata.metadataEtag": "W/"60af-tCc45VlNT7csKPRiK0amTPyY35E"",
"@odata.metadataEtag": "W/"6309-CcQCLrzrpArd+m2Ag482j6C0HxA"",
"value": [
{
"kind": "EntitySet",
Expand Down Expand Up @@ -645,7 +654,7 @@ exports[`V4 Requestor can reload the data 1`] = `
exports[`V4 Requestor can update data through a call 1`] = `
{
"@odata.context": "../$metadata#RootElement/$entity",
"@odata.metadataEtag": "W/"60af-tCc45VlNT7csKPRiK0amTPyY35E"",
"@odata.metadataEtag": "W/"6309-CcQCLrzrpArd+m2Ag482j6C0HxA"",
"HasActiveEntity": false,
"HasDraftEntity": false,
"ID": 556,
Expand All @@ -662,7 +671,7 @@ exports[`V4 Requestor can update data through a call 1`] = `
exports[`V4 Requestor can update data through a call 2`] = `
{
"@odata.context": "$metadata#RootElement(ID=556)",
"@odata.metadataEtag": "W/"60af-tCc45VlNT7csKPRiK0amTPyY35E"",
"@odata.metadataEtag": "W/"6309-CcQCLrzrpArd+m2Ag482j6C0HxA"",
"HasActiveEntity": false,
"HasDraftEntity": false,
"ID": 556,
Expand All @@ -686,7 +695,7 @@ exports[`V4 Requestor can update data through a call 3`] = `
exports[`V4 Requestor can update data through a call 4`] = `
{
"@odata.context": "$metadata#RootElement(ID=556)/Prop1",
"@odata.metadataEtag": "W/"60af-tCc45VlNT7csKPRiK0amTPyY35E"",
"@odata.metadataEtag": "W/"6309-CcQCLrzrpArd+m2Ag482j6C0HxA"",
"HasActiveEntity": false,
"HasDraftEntity": false,
"ID": 556,
Expand All @@ -703,7 +712,7 @@ exports[`V4 Requestor can update data through a call 4`] = `
exports[`V4 Requestor get one data 1`] = `
{
"@odata.context": "$metadata#RootElement(ID=2)",
"@odata.metadataEtag": "W/"60af-tCc45VlNT7csKPRiK0amTPyY35E"",
"@odata.metadataEtag": "W/"6309-CcQCLrzrpArd+m2Ag482j6C0HxA"",
"HasActiveEntity": false,
"HasDraftEntity": false,
"ID": 2,
Expand All @@ -722,7 +731,7 @@ exports[`V4 Requestor get one data 2`] = `""`;
exports[`V4 Requestor get one data 3`] = `
{
"@odata.context": "$metadata#RootElement(ID=2)",
"@odata.metadataEtag": "W/"60af-tCc45VlNT7csKPRiK0amTPyY35E"",
"@odata.metadataEtag": "W/"6309-CcQCLrzrpArd+m2Ag482j6C0HxA"",
"HasActiveEntity": false,
"HasDraftEntity": false,
"ID": 2,
Expand Down
39 changes: 39 additions & 0 deletions packages/fe-mockserver-core/test/unit/__testData/RootElement.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,45 @@ module.exports = {
return `STRICT :: ${actionData.data}`;
}
return actionData.data;
case 'bound412Action':
if (odataRequest.requestContent.headers.prefer) {
if (keys['ID'] == '1' || keys['ID'] == '2') {
odataRequest.addResponseHeader('Preference-Applied', 'handling=strict');
this.throwError('Unable to execute the action due to a warning.', 412, {
error: {
code: 412,
message: 'Unable to execute the action due to a warning.',
details: [{ code: 'null', message: 'Unable to execute the action due to a warning.' }],
'@Common.numericSeverity': 4
}
});
} else if (keys['ID'] == '3' || keys['ID'] == '4') {
this.throwError('unbound transition error', 500, {
error: {
code: 500,
message: 'unbound transition error',
transition: true,
'@Common.numericSeverity': 4
}
});
} else {
this.throwError(
'412 executed',
000,
[{ code: '412', message: '412 executed', numericSeverity: 1 }],
true
);
}
} else {
this.throwError(
'412 executed',
000,
[{ code: '412', message: '412 executed', numericSeverity: 1 }],
true
);
}
break;

default:
this.throwError('Not implemented', 501, {
error: {
Expand Down
Loading

0 comments on commit 09d5a17

Please sign in to comment.