Skip to content

Commit 0020da3

Browse files
committed
feat(davinci-client): normalize error details
1 parent bf12db7 commit 0020da3

10 files changed

+193
-13
lines changed

.changeset/spicy-phones-jam.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@forgerock/davinci-client': minor
3+
---
4+
5+
Replace less valuable `details` property from error with `collectors`

packages/davinci-client/src/lib/client.store.ts

+5
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,11 @@ export async function davinci({ config }: { config: DaVinciConfig }) {
245245
return nodeSlice.selectors.selectError(state);
246246
},
247247

248+
getErrorCollectors: () => {
249+
const state = store.getState();
250+
return nodeSlice.selectors.selectErrorCollectors(state);
251+
},
252+
248253
/**
249254
* @method node - Selector to get the node from state
250255
* @returns {Node} - the current node from state

packages/davinci-client/src/lib/davinci.types.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -151,12 +151,13 @@ interface NestedErrorDetails {
151151
innerError?: {
152152
history?: string;
153153
unsatisfiedRequirements?: string[];
154+
failuresRemaining?: number;
154155
};
155156
}
156157

157158
export interface ErrorDetail {
158159
// Optional properties
159-
message: string;
160+
message?: string;
160161
rawResponse?: {
161162
_embedded?: {
162163
users?: Array<unknown>;

packages/davinci-client/src/lib/mock-data/davinci.error.mock.ts

+57
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,63 @@ export const error1e = {
226226
isResponseCompatibleWithMobileAndWebSdks: true,
227227
};
228228

229+
export const error1f = {
230+
interactionId: '17c6a5d5-31d8-4416-b9cf-7b5e4d8ed5b1',
231+
companyId: '02fb4743-189a-4bc7-9d6c-a919edfe6447',
232+
connectionId: '94141bf2f1b9b59a5f5365ff135e02bb',
233+
connectorId: 'pingOneSSOConnector',
234+
id: 'agjdg5vxr2',
235+
capabilityName: 'createUser',
236+
errorCategory: 'InvalidData',
237+
code: 400,
238+
cause: null,
239+
expected: true,
240+
message: 'uniquenessViolation username: is unique but a non-unique value is provided',
241+
httpResponseCode: 400,
242+
details: [
243+
{
244+
rawResponse: {
245+
id: 'cc33f141-18ea-4e94-ad2b-ce031df11b3a',
246+
code: 'INVALID_DATA',
247+
message: 'Validation Error : [email must be a well-formed email address]',
248+
details: [
249+
{
250+
code: 'INVALID_VALUE',
251+
target: 'password',
252+
message: 'The provided password did not match provisioned password',
253+
innerError: {
254+
failuresRemaining: 3,
255+
},
256+
},
257+
{
258+
code: 'INVALID_VALUE',
259+
target: 'email',
260+
message: 'must be a well-formed email address',
261+
},
262+
],
263+
},
264+
statusCode: 400,
265+
},
266+
{
267+
rawResponse: {
268+
id: '6a4b3730-348c-400c-ba38-e0d8e76621dc',
269+
code: 'INVALID_DATA',
270+
message:
271+
'The request could not be completed. One or more validation errors were in the request.',
272+
details: [
273+
{
274+
code: 'UNIQUENESS_VIOLATION',
275+
target: 'username',
276+
message: 'is unique but a non-unique value is provided',
277+
},
278+
],
279+
},
280+
statusCode: 400,
281+
},
282+
],
283+
isResponseCompatibleWithMobileAndWebSdks: true,
284+
};
285+
229286
export const error2a = {
230287
interactionId: '18bd524d-4afd-4f79-a0b5-a4f16b63bf48',
231288
code: 'requestTimedOut',

packages/davinci-client/src/lib/node.slice.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ describe('The node slice reducers', () => {
8080
},
8181
error: {
8282
code: ' Invalid username and/or password',
83+
collectors: [],
8384
message: ' Invalid username and/or password',
8485
internalHttpStatus: 400,
8586
status: 'error',

packages/davinci-client/src/lib/node.slice.ts

+12-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { createSlice } from '@reduxjs/toolkit';
77
* Import the needed reducers
88
*/
99
import { nodeCollectorReducer, updateCollectorValues } from './node.reducer.js';
10+
import { getCollectorErrors } from './node.utils.js';
1011

1112
/**
1213
* Import the types
@@ -79,7 +80,7 @@ export const nodeSlice = createSlice({
7980

8081
newState.error = {
8182
code: action.payload.data.code,
82-
details: action.payload.data.details,
83+
collectors: getCollectorErrors(action.payload.data),
8384
message: action.payload.data.message,
8485
internalHttpStatus: action.payload.data.httpResponseCode,
8586
status: 'error',
@@ -277,7 +278,7 @@ export const nodeSlice = createSlice({
277278
// Let's check if the node has a client and collectors
278279
if (state.status !== CONTINUE_STATUS) {
279280
console.error(
280-
`\`collectors are only available on nodes with \`status\` of ${CONTINUE_STATUS}`,
281+
`\`collectors\` are only available on nodes with \`status\` of ${CONTINUE_STATUS}`,
281282
);
282283
return [];
283284
}
@@ -287,7 +288,7 @@ export const nodeSlice = createSlice({
287288
// Let's check if the node has a client and collectors
288289
if (state.status !== CONTINUE_STATUS) {
289290
console.error(
290-
`\`collectors are only available on nodes with \`status\` of ${CONTINUE_STATUS}`,
291+
`\`collectors\` are only available on nodes with \`status\` of ${CONTINUE_STATUS}`,
291292
);
292293
return;
293294
}
@@ -296,6 +297,14 @@ export const nodeSlice = createSlice({
296297
selectError: (state) => {
297298
return state.error;
298299
},
300+
selectErrorCollectors: (state) => {
301+
if (state.status !== ERROR_STATUS) {
302+
console.error(
303+
`\`errorCollectors\` are only available on nodes with \`status\` of ${ERROR_STATUS}`,
304+
);
305+
}
306+
return state.error?.collectors || [];
307+
},
299308
selectServer: (state) => {
300309
return state.server;
301310
},

packages/davinci-client/src/lib/node.types.test-d.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,18 @@ describe('Node Types', () => {
4646
message: 'Test error',
4747
code: 'TEST_ERROR',
4848
status: 'error',
49-
details: [{ message: 'Detail message' } as ErrorDetail],
49+
collectors: [
50+
{
51+
code: 'INVALID_VALUE',
52+
target: 'newPassword',
53+
message: 'New password did not satisfy password policy requirements',
54+
},
55+
],
5056
internalHttpStatus: 400,
5157
type: 'argument_error',
5258
};
5359

54-
expectTypeOf<DaVinciError>().toHaveProperty('details').toBeNullable();
60+
expectTypeOf<DaVinciError>().toHaveProperty('collectors').toBeNullable();
5561
expectTypeOf<DaVinciError>().toHaveProperty('internalHttpStatus').toBeNullable();
5662
});
5763

packages/davinci-client/src/lib/node.types.ts

+13-7
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,9 @@ import type {
1111
ReadOnlyCollector,
1212
ValidatedTextCollector,
1313
} from './collector.types.js';
14-
import type { ErrorDetail, Links } from './davinci.types.js';
14+
import type { Links } from './davinci.types.js';
1515
import { GenericError } from './error.types.js';
1616

17-
export interface DaVinciError extends GenericError {
18-
details?: ErrorDetail[];
19-
internalHttpStatus?: number;
20-
status: 'error' | 'failure' | 'unknown';
21-
}
22-
2317
export type Collectors =
2418
| FlowCollector
2519
| PasswordCollector
@@ -33,6 +27,12 @@ export type Collectors =
3327
| ReadOnlyCollector
3428
| ValidatedTextCollector;
3529

30+
export interface CollectorErrors {
31+
code: string;
32+
message: string;
33+
target: string;
34+
}
35+
3636
export interface ContinueNode {
3737
cache: {
3838
key: string;
@@ -58,6 +58,12 @@ export interface ContinueNode {
5858
status: 'continue';
5959
}
6060

61+
export interface DaVinciError extends GenericError {
62+
collectors?: CollectorErrors[];
63+
internalHttpStatus?: number;
64+
status: 'error' | 'failure' | 'unknown';
65+
}
66+
6167
export interface ErrorNode {
6268
cache: {
6369
key: string;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { describe, it, expect } from 'vitest';
2+
3+
import { error0a, error1a, error1b, error1c, error1f } from './mock-data/davinci.error.mock.js';
4+
import { getCollectorErrors } from './node.utils.js';
5+
6+
describe('getCollectorErrors', () => {
7+
it('should return an empty array if the error details does not exist', () => {
8+
const errorResult = getCollectorErrors(error0a);
9+
expect(errorResult).toEqual([]);
10+
});
11+
it('should return an array of error details', () => {
12+
const errorResult = getCollectorErrors(error1a);
13+
expect(errorResult).toEqual([
14+
{
15+
code: 'INVALID_VALUE',
16+
target: 'password',
17+
message: 'The provided password did not match provisioned password',
18+
},
19+
]);
20+
});
21+
it('should return an empty array if the rawResponse code has bad new password', () => {
22+
const errorResult = getCollectorErrors(error1b);
23+
expect(errorResult).toEqual([
24+
{
25+
code: 'INVALID_VALUE',
26+
target: 'newPassword',
27+
message: 'New password did not satisfy password policy requirements',
28+
},
29+
]);
30+
});
31+
it('should return an empty array if the rawResponse does not have code property', () => {
32+
const errorResult = getCollectorErrors(error1c);
33+
expect(errorResult).toEqual([]);
34+
});
35+
it('should return an array of two errors', () => {
36+
const errorResult = getCollectorErrors(error1f);
37+
expect(errorResult).toEqual([
38+
{
39+
code: 'INVALID_VALUE',
40+
target: 'password',
41+
message: 'The provided password did not match provisioned password',
42+
},
43+
{
44+
code: 'INVALID_VALUE',
45+
target: 'email',
46+
message: 'must be a well-formed email address',
47+
},
48+
{
49+
code: 'UNIQUENESS_VIOLATION',
50+
target: 'username',
51+
message: 'is unique but a non-unique value is provided',
52+
},
53+
]);
54+
});
55+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { DavinciErrorResponse } from './davinci.types';
2+
import { CollectorErrors } from './node.types';
3+
4+
export function getCollectorErrors(error: DavinciErrorResponse) {
5+
const details = error.details;
6+
if (!details || !Array.isArray(details)) {
7+
return [];
8+
}
9+
return details.reduce((acc, next) => {
10+
if (!next.rawResponse) {
11+
return acc;
12+
}
13+
if (!next.rawResponse.code) {
14+
return acc;
15+
}
16+
if (next.rawResponse.code !== 'INVALID_DATA') {
17+
return acc;
18+
}
19+
if (!Array.isArray(next.rawResponse.details)) {
20+
return acc;
21+
}
22+
next.rawResponse.details.forEach((item): void => {
23+
if (!item.target) {
24+
return;
25+
}
26+
acc.push({
27+
code: item.code || '',
28+
message: item.message || '',
29+
target: item.target,
30+
});
31+
});
32+
33+
return acc;
34+
}, [] as CollectorErrors[]);
35+
}

0 commit comments

Comments
 (0)