Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(davinci-client): normalize error details #134

Merged
merged 1 commit into from
Feb 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/spicy-phones-jam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@forgerock/davinci-client': minor
---

Replace less valuable `details` property from error with `collectors`
5 changes: 5 additions & 0 deletions packages/davinci-client/src/lib/client.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,11 @@ export async function davinci({ config }: { config: DaVinciConfig }) {
return nodeSlice.selectors.selectError(state);
},

getErrorCollectors: () => {
const state = store.getState();
return nodeSlice.selectors.selectErrorCollectors(state);
},

/**
* @method node - Selector to get the node from state
* @returns {Node} - the current node from state
Expand Down
3 changes: 2 additions & 1 deletion packages/davinci-client/src/lib/davinci.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,12 +151,13 @@ interface NestedErrorDetails {
innerError?: {
history?: string;
unsatisfiedRequirements?: string[];
failuresRemaining?: number;
};
}

export interface ErrorDetail {
// Optional properties
message: string;
message?: string;
rawResponse?: {
_embedded?: {
users?: Array<unknown>;
Expand Down
57 changes: 57 additions & 0 deletions packages/davinci-client/src/lib/mock-data/davinci.error.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,63 @@ export const error1e = {
isResponseCompatibleWithMobileAndWebSdks: true,
};

export const error1f = {
interactionId: '17c6a5d5-31d8-4416-b9cf-7b5e4d8ed5b1',
companyId: '02fb4743-189a-4bc7-9d6c-a919edfe6447',
connectionId: '94141bf2f1b9b59a5f5365ff135e02bb',
connectorId: 'pingOneSSOConnector',
id: 'agjdg5vxr2',
capabilityName: 'createUser',
errorCategory: 'InvalidData',
code: 400,
cause: null,
expected: true,
message: 'uniquenessViolation username: is unique but a non-unique value is provided',
httpResponseCode: 400,
details: [
{
rawResponse: {
id: 'cc33f141-18ea-4e94-ad2b-ce031df11b3a',
code: 'INVALID_DATA',
message: 'Validation Error : [email must be a well-formed email address]',
details: [
{
code: 'INVALID_VALUE',
target: 'password',
message: 'The provided password did not match provisioned password',
innerError: {
failuresRemaining: 3,
},
},
{
code: 'INVALID_VALUE',
target: 'email',
message: 'must be a well-formed email address',
},
],
},
statusCode: 400,
},
{
rawResponse: {
id: '6a4b3730-348c-400c-ba38-e0d8e76621dc',
code: 'INVALID_DATA',
message:
'The request could not be completed. One or more validation errors were in the request.',
details: [
{
code: 'UNIQUENESS_VIOLATION',
target: 'username',
message: 'is unique but a non-unique value is provided',
},
],
},
statusCode: 400,
},
],
isResponseCompatibleWithMobileAndWebSdks: true,
};

export const error2a = {
interactionId: '18bd524d-4afd-4f79-a0b5-a4f16b63bf48',
code: 'requestTimedOut',
Expand Down
1 change: 1 addition & 0 deletions packages/davinci-client/src/lib/node.slice.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ describe('The node slice reducers', () => {
},
error: {
code: ' Invalid username and/or password',
collectors: [],
message: ' Invalid username and/or password',
internalHttpStatus: 400,
status: 'error',
Expand Down
15 changes: 12 additions & 3 deletions packages/davinci-client/src/lib/node.slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { createSlice } from '@reduxjs/toolkit';
* Import the needed reducers
*/
import { nodeCollectorReducer, updateCollectorValues } from './node.reducer.js';
import { getCollectorErrors } from './node.utils.js';

/**
* Import the types
Expand Down Expand Up @@ -79,7 +80,7 @@ export const nodeSlice = createSlice({

newState.error = {
code: action.payload.data.code,
details: action.payload.data.details,
collectors: getCollectorErrors(action.payload.data),
message: action.payload.data.message,
internalHttpStatus: action.payload.data.httpResponseCode,
status: 'error',
Expand Down Expand Up @@ -277,7 +278,7 @@ export const nodeSlice = createSlice({
// Let's check if the node has a client and collectors
if (state.status !== CONTINUE_STATUS) {
console.error(
`\`collectors are only available on nodes with \`status\` of ${CONTINUE_STATUS}`,
`\`collectors\` are only available on nodes with \`status\` of ${CONTINUE_STATUS}`,
);
return [];
}
Expand All @@ -287,7 +288,7 @@ export const nodeSlice = createSlice({
// Let's check if the node has a client and collectors
if (state.status !== CONTINUE_STATUS) {
console.error(
`\`collectors are only available on nodes with \`status\` of ${CONTINUE_STATUS}`,
`\`collectors\` are only available on nodes with \`status\` of ${CONTINUE_STATUS}`,
);
return;
}
Expand All @@ -296,6 +297,14 @@ export const nodeSlice = createSlice({
selectError: (state) => {
return state.error;
},
selectErrorCollectors: (state) => {
if (state.status !== ERROR_STATUS) {
console.error(
`\`errorCollectors\` are only available on nodes with \`status\` of ${ERROR_STATUS}`,
);
}
return state.error?.collectors || [];
},
selectServer: (state) => {
return state.server;
},
Expand Down
10 changes: 8 additions & 2 deletions packages/davinci-client/src/lib/node.types.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,18 @@ describe('Node Types', () => {
message: 'Test error',
code: 'TEST_ERROR',
status: 'error',
details: [{ message: 'Detail message' } as ErrorDetail],
collectors: [
{
code: 'INVALID_VALUE',
target: 'newPassword',
message: 'New password did not satisfy password policy requirements',
},
],
internalHttpStatus: 400,
type: 'argument_error',
};

expectTypeOf<DaVinciError>().toHaveProperty('details').toBeNullable();
expectTypeOf<DaVinciError>().toHaveProperty('collectors').toBeNullable();
expectTypeOf<DaVinciError>().toHaveProperty('internalHttpStatus').toBeNullable();
});

Expand Down
20 changes: 13 additions & 7 deletions packages/davinci-client/src/lib/node.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,9 @@ import type {
ReadOnlyCollector,
ValidatedTextCollector,
} from './collector.types.js';
import type { ErrorDetail, Links } from './davinci.types.js';
import type { Links } from './davinci.types.js';
import { GenericError } from './error.types.js';

export interface DaVinciError extends GenericError {
details?: ErrorDetail[];
internalHttpStatus?: number;
status: 'error' | 'failure' | 'unknown';
}

export type Collectors =
| FlowCollector
| PasswordCollector
Expand All @@ -33,6 +27,12 @@ export type Collectors =
| ReadOnlyCollector
| ValidatedTextCollector;

export interface CollectorErrors {
code: string;
message: string;
target: string;
}

export interface ContinueNode {
cache: {
key: string;
Expand All @@ -58,6 +58,12 @@ export interface ContinueNode {
status: 'continue';
}

export interface DaVinciError extends GenericError {
collectors?: CollectorErrors[];
internalHttpStatus?: number;
status: 'error' | 'failure' | 'unknown';
}

export interface ErrorNode {
cache: {
key: string;
Expand Down
55 changes: 55 additions & 0 deletions packages/davinci-client/src/lib/node.utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { describe, it, expect } from 'vitest';

import { error0a, error1a, error1b, error1c, error1f } from './mock-data/davinci.error.mock.js';
import { getCollectorErrors } from './node.utils.js';

describe('getCollectorErrors', () => {
it('should return an empty array if the error details does not exist', () => {
const errorResult = getCollectorErrors(error0a);
expect(errorResult).toEqual([]);
});
it('should return an array of error details', () => {
const errorResult = getCollectorErrors(error1a);
expect(errorResult).toEqual([
{
code: 'INVALID_VALUE',
target: 'password',
message: 'The provided password did not match provisioned password',
},
]);
});
it('should return an empty array if the rawResponse code has bad new password', () => {
const errorResult = getCollectorErrors(error1b);
expect(errorResult).toEqual([
{
code: 'INVALID_VALUE',
target: 'newPassword',
message: 'New password did not satisfy password policy requirements',
},
]);
});
it('should return an empty array if the rawResponse does not have code property', () => {
const errorResult = getCollectorErrors(error1c);
expect(errorResult).toEqual([]);
});
it('should return an array of two errors', () => {
const errorResult = getCollectorErrors(error1f);
expect(errorResult).toEqual([
{
code: 'INVALID_VALUE',
target: 'password',
message: 'The provided password did not match provisioned password',
},
{
code: 'INVALID_VALUE',
target: 'email',
message: 'must be a well-formed email address',
},
{
code: 'UNIQUENESS_VIOLATION',
target: 'username',
message: 'is unique but a non-unique value is provided',
},
]);
});
});
35 changes: 35 additions & 0 deletions packages/davinci-client/src/lib/node.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { DavinciErrorResponse } from './davinci.types';
import { CollectorErrors } from './node.types';

export function getCollectorErrors(error: DavinciErrorResponse) {
const details = error.details;
if (!details || !Array.isArray(details)) {
return [];
}
return details.reduce<CollectorErrors[]>((acc, next) => {
if (!next.rawResponse) {
return acc;
}
if (!next.rawResponse.code) {
return acc;
}
if (next.rawResponse.code !== 'INVALID_DATA') {
return acc;
}
if (!Array.isArray(next.rawResponse.details)) {
return acc;
}
next.rawResponse.details.forEach((item): void => {
if (!item.target) {
return;
}
acc.push({
code: item.code || '',
message: item.message || '',
target: item.target,
});
});

return acc;
}, []);
}
Loading