From df7f0d6d0994c81be76acdb5981c8c30f7af06f8 Mon Sep 17 00:00:00 2001 From: Volker Scheuber Date: Sun, 7 Apr 2024 22:05:50 -0500 Subject: [PATCH] fixes #392 --- CHANGELOG.md | 4 + src/api/IdmConfigApi.ts | 8 +- src/ops/ApplicationOps.ts | 9 +- src/ops/AuthenticateOps.ts | 2 +- src/ops/CirclesOfTrustOps.ts | 11 +- src/ops/ConfigOps.ts | 410 ++++++++++++------ src/ops/ConnectorOps.ts | 5 - src/ops/EmailTemplateOps.ts | 5 - src/ops/FrodoError.ts | 17 +- src/ops/IdmConfigOps.ts | 105 +++-- src/ops/IdpOps.ts | 5 - src/ops/JourneyOps.ts | 5 - src/ops/MappingOps.ts | 5 - src/ops/OAuth2ClientOps.ts | 7 +- src/ops/OAuth2TrustedJwtIssuerOps.ts | 5 - src/ops/PolicyOps.ts | 5 - src/ops/PolicySetOps.ts | 6 - src/ops/ResourceTypeOps.ts | 5 - src/ops/Saml2Ops.ts | 14 +- src/ops/ScriptOps.test.ts | 11 +- src/ops/ScriptOps.ts | 10 +- src/ops/ServiceOps.ts | 2 +- src/ops/cloud/AdminFederationOps.ts | 5 - src/test/snapshots/ops/PolicyOps.test.js.snap | 3 + .../snapshots/ops/PolicySetOps.test.js.snap | 5 + .../ops/ResourceTypeOps.test.js.snap | 5 + src/test/snapshots/ops/ScriptOps.test.js.snap | 22 +- src/utils/ExportImportUtils.ts | 17 +- 28 files changed, 416 insertions(+), 297 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c91526050..035d2a518 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- \#392: Implemented error handling pattern for methods with unusual amounts of REST calls like `frodo.config.exportFullConfiguration` and `frodo.config.importFullConfiguration` + ## [2.0.0-75] - 2024-03-29 ### Fixed diff --git a/src/api/IdmConfigApi.ts b/src/api/IdmConfigApi.ts index 54cdea8a0..94d7d0eb0 100644 --- a/src/api/IdmConfigApi.ts +++ b/src/api/IdmConfigApi.ts @@ -122,12 +122,8 @@ export async function putConfigEntity({ getHostBaseUrl(state.getHost()), entityId ); - try { - const { data } = await generateIdmApi({ state }).put(urlString, entityData); - return data; - } catch (error) { - throw Error(`Error on config entity ${entityId}: ${error}`); - } + const { data } = await generateIdmApi({ state }).put(urlString, entityData); + return data; } /** diff --git a/src/ops/ApplicationOps.ts b/src/ops/ApplicationOps.ts index a528d4a8b..e60506ef0 100644 --- a/src/ops/ApplicationOps.ts +++ b/src/ops/ApplicationOps.ts @@ -1381,7 +1381,6 @@ export async function importApplications({ }): Promise { const response = []; const errors = []; - const imported = []; try { for (const applicationId of Object.keys(importData.managedApplication)) { const applicationData = importData.managedApplication[applicationId]; @@ -1402,7 +1401,6 @@ export async function importApplications({ state, }) ); - imported.push(applicationId); } catch (error) { errors.push(error); } @@ -1410,15 +1408,10 @@ export async function importApplications({ if (errors.length) { throw new FrodoError(`Error importing applications`, errors); } - if (0 === imported.length) { - throw new FrodoError( - `Import error:\nNo applications found in import data!` - ); - } return response; } catch (error) { // just re-throw previously caught errors - if (errors.length > 0 || imported.length == 0) { + if (errors.length > 0) { throw error; } throw new FrodoError(`Error importing applications`, error); diff --git a/src/ops/AuthenticateOps.ts b/src/ops/AuthenticateOps.ts index 355d07499..0b81fb4fa 100644 --- a/src/ops/AuthenticateOps.ts +++ b/src/ops/AuthenticateOps.ts @@ -715,7 +715,7 @@ export async function getFreshSaBearerToken({ const err: FrodoError = error as FrodoError; if ( err.isHttpError && - err.httpErrorMessage === 'invalid_scope' && + err.httpErrorText === 'invalid_scope' && err.httpDescription?.startsWith('Unsupported scope for service account: ') ) { const invalidScopes: string[] = err.httpDescription diff --git a/src/ops/CirclesOfTrustOps.ts b/src/ops/CirclesOfTrustOps.ts index 11bf3bb23..eb908171e 100644 --- a/src/ops/CirclesOfTrustOps.ts +++ b/src/ops/CirclesOfTrustOps.ts @@ -707,7 +707,6 @@ export async function importCirclesOfTrust({ }): Promise { const responses = []; const errors = []; - const imported = []; try { entityProviders = entityProviders.map((id) => `${id}|saml2`); const validEntityIds = await readSaml2EntityIds({ state }); @@ -736,7 +735,6 @@ export async function importCirclesOfTrust({ cotData, state, }); - imported.push(cotId); responses.push(response); } catch (createError) { if ((createError as FrodoError).httpStatus === 409) { @@ -770,7 +768,6 @@ export async function importCirclesOfTrust({ cotData: existingCot, state, }); - imported.push(cotId); responses.push(response); } else { debugMessage({ @@ -848,20 +845,14 @@ export async function importCirclesOfTrust({ }); errors.push(error); } - imported.push(cotId); } if (errors.length > 0) { throw new FrodoError(`Error importing circles of trust`); } - if (0 === imported.length) { - throw new Error( - `Import error:\nNo circles of trust found in import data!` - ); - } return responses; } catch (error) { // just re-throw previously caught errors - if (errors.length > 0 || imported.length == 0) { + if (errors.length > 0) { throw error; } throw new FrodoError(`Error importing circles of trust`, error); diff --git a/src/ops/ConfigOps.ts b/src/ops/ConfigOps.ts index 5a83b89b6..d1452b976 100644 --- a/src/ops/ConfigOps.ts +++ b/src/ops/ConfigOps.ts @@ -44,6 +44,7 @@ import { exportEmailTemplates, importEmailTemplates, } from './EmailTemplateOps'; +import { FrodoError } from './FrodoError'; import { exportConfigEntities, importConfigEntities } from './IdmConfigOps'; import { exportSocialIdentityProviders, @@ -68,19 +69,23 @@ export type Config = { /** * Export full configuration * @param {FullExportOptions} options export options + * @param {Error[]} collectErrors optional parameters to collect erros instead of having the function throw. Pass an empty array to collect errors and report on them but have the function perform all it can and return the export data even if it encounters errors. * @returns {Promise} a promise resolving to a full export object */ exportFullConfiguration( - options: FullExportOptions + options: FullExportOptions, + collectErrors?: Error[] ): Promise; /** * Import full configuration * @param {FullExportInterface} importData import data * @param {FullImportOptions} options import options + * @param {Error[]} collectErrors optional parameters to collect erros instead of having the function throw. Pass an empty array to collect errors and report on them but have the function perform all it can and return the export data even if it encounters errors. */ importFullConfiguration( importData: FullExportInterface, - options: FullImportOptions + options: FullImportOptions, + collectErrors?: Error[] ): Promise; }; @@ -92,9 +97,10 @@ export default (state: State): Config => { noDecode: false, coords: true, includeDefault: false, - } + }, + collectErrors: Error[] ) { - return exportFullConfiguration({ options, state }); + return exportFullConfiguration({ options, collectErrors, state }); }, async importFullConfiguration( importData: FullExportInterface, @@ -105,11 +111,13 @@ export default (state: State): Config => { global: false, realm: false, includeDefault: false, - } + }, + collectErrors: Error[] ) { return importFullConfiguration({ importData, options, + collectErrors, state, }); }, @@ -207,23 +215,33 @@ export async function exportFullConfiguration({ coords: true, includeDefault: false, }, + collectErrors, state, }: { options: FullExportOptions; + collectErrors?: Error[]; state: State; }): Promise { + let errors: Error[] = []; + let throwErrors: boolean = true; + if (collectErrors && Array.isArray(collectErrors)) { + throwErrors = false; + errors = collectErrors; + } const { useStringArrays, noDecode, coords, includeDefault } = options; const stateObj = { state }; //Export saml2 providers and circle of trusts let saml = ( (await exportOrImportWithErrorHandling( exportSaml2Providers, - stateObj + stateObj, + errors )) as CirclesOfTrustExportInterface )?.saml; const cotExport = await exportOrImportWithErrorHandling( exportCirclesOfTrust, - stateObj + stateObj, + errors ); if (saml) { saml.cot = cotExport?.saml.cot; @@ -231,93 +249,150 @@ export async function exportFullConfiguration({ saml = cotExport?.saml; } //Create full export - return { + const fullExport = { meta: getMetadata(stateObj), - agents: (await exportOrImportWithErrorHandling(exportAgents, stateObj)) - ?.agents, + agents: ( + await exportOrImportWithErrorHandling(exportAgents, stateObj, errors) + )?.agents, application: ( - await exportOrImportWithErrorHandling(exportOAuth2Clients, { - options: { deps: false, useStringArrays }, - state, - }) + await exportOrImportWithErrorHandling( + exportOAuth2Clients, + { + options: { deps: false, useStringArrays }, + state, + }, + errors + ) )?.application, authentication: ( await exportOrImportWithErrorHandling( exportAuthenticationSettings, - stateObj + stateObj, + errors ) )?.authentication, config: ( - await exportOrImportWithErrorHandling(exportConfigEntities, stateObj) + await exportOrImportWithErrorHandling( + exportConfigEntities, + stateObj, + errors + ) )?.config, emailTemplate: ( - await exportOrImportWithErrorHandling(exportEmailTemplates, stateObj) + await exportOrImportWithErrorHandling( + exportEmailTemplates, + stateObj, + errors + ) )?.emailTemplate, idp: ( await exportOrImportWithErrorHandling( exportSocialIdentityProviders, - stateObj + stateObj, + errors ) )?.idp, managedApplication: ( - await exportOrImportWithErrorHandling(exportApplications, { - options: { deps: false, useStringArrays }, - state, - }) + await exportOrImportWithErrorHandling( + exportApplications, + { + options: { deps: false, useStringArrays }, + state, + }, + errors + ) )?.managedApplication, policy: ( - await exportOrImportWithErrorHandling(exportPolicies, { - options: { deps: false, prereqs: false, useStringArrays }, - state, - }) + await exportOrImportWithErrorHandling( + exportPolicies, + { + options: { deps: false, prereqs: false, useStringArrays }, + state, + }, + errors + ) )?.policy, policyset: ( - await exportOrImportWithErrorHandling(exportPolicySets, { - options: { deps: false, prereqs: false, useStringArrays }, - state, - }) + await exportOrImportWithErrorHandling( + exportPolicySets, + { + options: { deps: false, prereqs: false, useStringArrays }, + state, + }, + errors + ) )?.policyset, resourcetype: ( - await exportOrImportWithErrorHandling(exportResourceTypes, stateObj) + await exportOrImportWithErrorHandling( + exportResourceTypes, + stateObj, + errors + ) )?.resourcetype, saml, script: ( - await exportOrImportWithErrorHandling(exportScripts, { - includeDefault, - state, - }) + await exportOrImportWithErrorHandling( + exportScripts, + { + includeDefault, + state, + }, + errors + ) )?.script, - secrets: (await exportOrImportWithErrorHandling(exportSecrets, stateObj)) - ?.secrets, + secrets: ( + await exportOrImportWithErrorHandling(exportSecrets, stateObj, errors) + )?.secrets, service: { ...( - await exportOrImportWithErrorHandling(exportServices, { - globalConfig: true, - state, - }) + await exportOrImportWithErrorHandling( + exportServices, + { + globalConfig: true, + state, + }, + errors + ) )?.service, ...( - await exportOrImportWithErrorHandling(exportServices, { - globalConfig: false, - state, - }) + await exportOrImportWithErrorHandling( + exportServices, + { + globalConfig: false, + state, + }, + errors + ) )?.service, }, - theme: (await exportOrImportWithErrorHandling(exportThemes, stateObj)) - ?.theme, + theme: ( + await exportOrImportWithErrorHandling(exportThemes, stateObj, errors) + )?.theme, trees: ( - await exportOrImportWithErrorHandling(exportJourneys, { - options: { deps: false, useStringArrays, coords }, - state, - }) + await exportOrImportWithErrorHandling( + exportJourneys, + { + options: { deps: false, useStringArrays, coords }, + state, + }, + errors + ) )?.trees, variables: ( - await exportOrImportWithErrorHandling(exportVariables, { - noDecode, - state, - }) + await exportOrImportWithErrorHandling( + exportVariables, + { + noDecode, + state, + }, + errors + ) )?.variables, }; + if (throwErrors && errors.length > 0) { + throw new FrodoError(`Error exporting full config`, errors); + } + return fullExport; } /** @@ -335,12 +410,20 @@ export async function importFullConfiguration({ realm: false, includeDefault: false, }, + collectErrors, state, }: { importData: FullExportInterface; options: FullImportOptions; + collectErrors?: Error[]; state: State; }): Promise { + let errors: Error[] = []; + let throwErrors: boolean = true; + if (collectErrors && Array.isArray(collectErrors)) { + throwErrors = false; + errors = collectErrors; + } const { reUuidJourneys, reUuidScripts, @@ -360,161 +443,228 @@ export async function importFullConfiguration({ message: `Importing Scripts...`, state, }); - await exportOrImportWithErrorHandling(importScripts, { - scriptName: '', - importData, - options: { - reUuid: reUuidScripts, - includeDefault, + await exportOrImportWithErrorHandling( + importScripts, + { + scriptName: '', + importData, + options: { + reUuid: reUuidScripts, + includeDefault, + }, + validate: false, + state, }, - validate: false, - state, - }); + errors + ); updateProgressIndicator({ id: indicatorId, message: `Importing Authentication Settings...`, state, }); - await exportOrImportWithErrorHandling(importAuthenticationSettings, { - importData, - state, - }); + await exportOrImportWithErrorHandling( + importAuthenticationSettings, + { + importData, + state, + }, + errors + ); updateProgressIndicator({ id: indicatorId, message: `Importing Agents...`, state, }); - await exportOrImportWithErrorHandling(importAgents, { importData, state }); + await exportOrImportWithErrorHandling( + importAgents, + { importData, state }, + errors + ); updateProgressIndicator({ id: indicatorId, message: `Importing IDM Config Entities...`, state, }); - await exportOrImportWithErrorHandling(importConfigEntities, { - importData, - options: { validate: false }, - state, - }); + await exportOrImportWithErrorHandling( + importConfigEntities, + { + importData, + options: { validate: false }, + state, + }, + errors + ); updateProgressIndicator({ id: indicatorId, message: `Importing Email Templates...`, state, }); - await exportOrImportWithErrorHandling(importEmailTemplates, { - importData, - state, - }); + await exportOrImportWithErrorHandling( + importEmailTemplates, + { + importData, + state, + }, + errors + ); updateProgressIndicator({ id: indicatorId, message: `Importing Resource Types...`, state, }); - await exportOrImportWithErrorHandling(importResourceTypes, { - importData, - state, - }); + await exportOrImportWithErrorHandling( + importResourceTypes, + { + importData, + state, + }, + errors + ); updateProgressIndicator({ id: indicatorId, message: `Importing Circles of Trust...`, state, }); - await exportOrImportWithErrorHandling(importCirclesOfTrust, { - importData, - state, - }); + await exportOrImportWithErrorHandling( + importCirclesOfTrust, + { + importData, + state, + }, + errors + ); updateProgressIndicator({ id: indicatorId, message: `Importing Services...`, state, }); - await exportOrImportWithErrorHandling(importServices, { - importData, - options: { clean: cleanServices, global, realm }, - state, - }); + await exportOrImportWithErrorHandling( + importServices, + { + importData, + options: { clean: cleanServices, global, realm }, + state, + }, + errors + ); updateProgressIndicator({ id: indicatorId, message: `Importing Themes...`, state, }); - await exportOrImportWithErrorHandling(importThemes, { - importData, - state, - }); + await exportOrImportWithErrorHandling( + importThemes, + { + importData, + state, + }, + errors + ); updateProgressIndicator({ id: indicatorId, message: `Importing Saml2 Providers...`, state, }); - await exportOrImportWithErrorHandling(importSaml2Providers, { - importData, - options: { deps: false }, - state, - }); + await exportOrImportWithErrorHandling( + importSaml2Providers, + { + importData, + options: { deps: false }, + state, + }, + errors + ); updateProgressIndicator({ id: indicatorId, message: `Importing Social Identity Providers...`, state, }); - await exportOrImportWithErrorHandling(importSocialIdentityProviders, { - importData, - options: { deps: false }, - state, - }); + await exportOrImportWithErrorHandling( + importSocialIdentityProviders, + { + importData, + options: { deps: false }, + state, + }, + errors + ); updateProgressIndicator({ id: indicatorId, message: `Importing OAuth2 Clients...`, state, }); - await exportOrImportWithErrorHandling(importOAuth2Clients, { - importData, - options: { deps: false }, - state, - }); + await exportOrImportWithErrorHandling( + importOAuth2Clients, + { + importData, + options: { deps: false }, + state, + }, + errors + ); updateProgressIndicator({ id: indicatorId, message: `Importing Applications...`, state, }); - await exportOrImportWithErrorHandling(importApplications, { - importData, - options: { deps: false }, - state, - }); + await exportOrImportWithErrorHandling( + importApplications, + { + importData, + options: { deps: false }, + state, + }, + errors + ); updateProgressIndicator({ id: indicatorId, message: `Importing Policy Sets...`, state, }); - await exportOrImportWithErrorHandling(importPolicySets, { - importData, - options: { deps: false, prereqs: false }, - state, - }); + await exportOrImportWithErrorHandling( + importPolicySets, + { + importData, + options: { deps: false, prereqs: false }, + state, + }, + errors + ); updateProgressIndicator({ id: indicatorId, message: `Importing Policies...`, state, }); - await exportOrImportWithErrorHandling(importPolicies, { - importData, - options: { deps: false, prereqs: false }, - state, - }); + await exportOrImportWithErrorHandling( + importPolicies, + { + importData, + options: { deps: false, prereqs: false }, + state, + }, + errors + ); updateProgressIndicator({ id: indicatorId, message: `Importing Journeys...`, state, }); - await exportOrImportWithErrorHandling(importJourneys, { - importData, - options: { deps: false, reUuid: reUuidJourneys }, - state, - }); + await exportOrImportWithErrorHandling( + importJourneys, + { + importData, + options: { deps: false, reUuid: reUuidJourneys }, + state, + }, + errors + ); stopProgressIndicator({ id: indicatorId, message: 'Finished Importing Everything!', status: 'success', state, }); + if (throwErrors && errors.length > 0) { + throw new FrodoError(`Error importing full config`, errors); + } } diff --git a/src/ops/ConnectorOps.ts b/src/ops/ConnectorOps.ts index 38fef929f..d9267b748 100644 --- a/src/ops/ConnectorOps.ts +++ b/src/ops/ConnectorOps.ts @@ -681,7 +681,6 @@ export async function importConnectors({ }): Promise { const response = []; const errors = []; - const imported = []; for (const key of Object.keys(importData.connector)) { try { if (options.deps) { @@ -694,7 +693,6 @@ export async function importConnectors({ state, }) ); - imported.push(key); } catch (error) { errors.push(error); } @@ -702,8 +700,5 @@ export async function importConnectors({ if (errors.length > 0) { throw new FrodoError(`Error importing connectors`, errors); } - if (0 === imported.length) { - throw new FrodoError(`No connectors not found in import data!`); - } return response; } diff --git a/src/ops/EmailTemplateOps.ts b/src/ops/EmailTemplateOps.ts index cd6e90755..87fa49e96 100644 --- a/src/ops/EmailTemplateOps.ts +++ b/src/ops/EmailTemplateOps.ts @@ -392,7 +392,6 @@ export async function importEmailTemplates({ }); const response = []; const errors = []; - const imported = []; for (const templateId of Object.keys(importData.emailTemplate)) { try { debugMessage({ @@ -406,7 +405,6 @@ export async function importEmailTemplates({ state, }) ); - imported.push(templateId); } catch (e) { errors.push(e); } @@ -414,9 +412,6 @@ export async function importEmailTemplates({ if (errors.length > 0) { throw new FrodoError(`Error importing email templates`, errors); } - if (0 === imported.length) { - throw new FrodoError(`No email templates found in import data`); - } debugMessage({ message: `EmailTemplateOps.importEmailTemplates: end`, state, diff --git a/src/ops/FrodoError.ts b/src/ops/FrodoError.ts index 2e3f37063..d82e868be 100644 --- a/src/ops/FrodoError.ts +++ b/src/ops/FrodoError.ts @@ -5,7 +5,8 @@ export class FrodoError extends Error { httpStatus: number; httpMessage: string; httpDetail: string; - httpErrorMessage: string; + httpErrorText: string; + httpErrorReason: string; httpDescription: string; constructor(message: string, originalErrors: Error | Error[] = null) { @@ -40,11 +41,16 @@ export class FrodoError extends Error { ? error['response'].data.detail : null : null; - this.httpErrorMessage = error['response'] + this.httpErrorText = error['response'] ? error['response'].data ? error['response'].data.error : null : null; + this.httpErrorReason = error['response'] + ? error['response'].data + ? error['response'].data.reason + : null + : null; this.httpDescription = error['response'] ? error['response'].data ? error['response'].data.error_description @@ -75,8 +81,11 @@ export class FrodoError extends Error { combinedMessage += this.httpStatus ? `\n Status: ${this.httpStatus}` : ''; - combinedMessage += this.httpErrorMessage - ? `\n Error: ${this.httpErrorMessage}` + combinedMessage += this.httpErrorText + ? `\n Error: ${this.httpErrorText}` + : ''; + combinedMessage += this.httpErrorReason + ? `\n Reason: ${this.httpErrorReason}` : ''; combinedMessage += this.httpMessage ? `\n Message: ${this.httpMessage}` diff --git a/src/ops/IdmConfigOps.ts b/src/ops/IdmConfigOps.ts index 2d534ff7a..04634bc7e 100644 --- a/src/ops/IdmConfigOps.ts +++ b/src/ops/IdmConfigOps.ts @@ -13,6 +13,7 @@ import { putConfigEntity as _putConfigEntity, } from '../api/IdmConfigApi'; import { ConnectorServerStatusInterface } from '../api/IdmSystemApi'; +import Constants from '../shared/Constants'; import { State } from '../shared/State'; import { createProgressIndicator, @@ -364,12 +365,38 @@ export async function readConfigEntity({ state: State; }): Promise { try { - return getConfigEntity({ entityId, state }); + const result = await getConfigEntity({ entityId, state }); + return result; } catch (error) { throw new FrodoError(`Error reading config entity ${entityId}`, error); } } +const AIC_PROTECTED_ENTITIES: string[] = [ + 'emailTemplate/frEmailUpdated', + 'emailTemplate/frForgotUsername', + 'emailTemplate/frOnboarding', + 'emailTemplate/frPasswordUpdated', + 'emailTemplate/frProfileUpdated', + 'emailTemplate/frResetPassword', + 'emailTemplate/frUsernameUpdated', +]; + +const IDM_UNAVAILABLE_ENTITIES = [ + 'script', + 'notificationFactory', + 'apiVersion', + 'metrics', + 'repo.init', + 'endpoint/validateQueryFilter', + 'endpoint/oauthproxy', + 'external.rest', + 'scheduler', + 'org.apache.felix.fileinstall/openidm', + 'cluster', + 'endpoint/mappingDetails', + 'fieldPolicy/teammember', +]; /** * Export all IDM config entities * @returns {ConfigEntityExportInterface} promise resolving to a ConfigEntityExportInterface object @@ -397,38 +424,24 @@ export async function exportConfigEntities({ entityPromises.push( readConfigEntity({ entityId: configEntity._id, state }).catch( (readConfigEntityError) => { + const error: FrodoError = readConfigEntityError; if ( + // operation is not available in ForgeRock Identity Cloud !( - readConfigEntityError.response?.status === 403 && - readConfigEntityError.response?.data?.message === + error.httpStatus === 403 && + error.httpMessage === 'This operation is not available in ForgeRock Identity Cloud.' ) && + // list of config entities, which do not exist by default or ever. !( - // list of config entities, which do not exist by default or ever. - ( - [ - 'script', - 'notificationFactory', - 'apiVersion', - 'metrics', - 'repo.init', - 'endpoint/validateQueryFilter', - 'endpoint/oauthproxy', - 'external.rest', - 'scheduler', - 'org.apache.felix.fileinstall/openidm', - 'cluster', - 'endpoint/mappingDetails', - 'fieldPolicy/teammember', - ].includes(configEntity._id) && - readConfigEntityError.response?.status === 404 && - readConfigEntityError.response?.data?.reason === 'Not Found' - ) + IDM_UNAVAILABLE_ENTITIES.includes(configEntity._id) && + error.httpStatus === 404 && + error.httpErrorReason === 'Not Found' ) && // https://bugster.forgerock.org/jira/browse/OPENIDM-18270 !( - readConfigEntityError.response?.status === 404 && - readConfigEntityError.response?.data?.message === + error.httpStatus === 404 && + error.httpMessage === 'No configuration exists for id org.apache.felix.fileinstall/openidm' ) ) { @@ -504,7 +517,8 @@ export async function updateConfigEntity({ state: State; }): Promise { try { - return _putConfigEntity({ entityId, entityData, state }); + const result = await _putConfigEntity({ entityId, entityData, state }); + return result; } catch (error) { throw new FrodoError(`Error updating config entity ${entityId}`, error); } @@ -522,7 +536,6 @@ export async function importConfigEntities({ debugMessage({ message: `IdmConfigOps.importConfigEntities: start`, state }); const response = []; const errors = []; - const imported = []; for (const entityId of Object.keys(importData.config)) { try { debugMessage({ @@ -534,29 +547,37 @@ export async function importConfigEntities({ options.validate && !areScriptHooksValid({ jsonData: entityData, state }) ) { - errors.push( - Error(`Invalid script hook in the config object '${entityId}'`) + throw new FrodoError( + `Invalid script hook in the config object '${entityId}'` ); - } else { - const result: IdObjectSkeletonInterface | PromiseRejectedResult = - await updateConfigEntity({ entityId, entityData, state }); - if (result.status === 'rejected') { - errors.push(Error(`- ${result.reason}`)); - } else { - response.push(result); + } + try { + const result = await updateConfigEntity({ + entityId, + entityData, + state, + }); + response.push(result); + } catch (error) { + if ( + // protected entities (e.g. root realm email templates) + !( + state.getDeploymentType() === Constants.CLOUD_DEPLOYMENT_TYPE_KEY && + AIC_PROTECTED_ENTITIES.includes(entityId) && + error.httpStatus === 403 && + error.httpCode === 'ERR_BAD_REQUEST' + ) + ) { + throw error; } } - imported.push(entityId); - } catch (e) { - errors.push(e); + } catch (error) { + errors.push(error); } } if (errors.length > 0) { throw new FrodoError(`Error importing config entities`, errors); } - if (0 === imported.length) { - throw new FrodoError(`No config entities found in import data!`); - } debugMessage({ message: `IdmConfigOps.importConfigEntities: end`, state }); return response; } diff --git a/src/ops/IdpOps.ts b/src/ops/IdpOps.ts index 46f95b263..ea1954a38 100644 --- a/src/ops/IdpOps.ts +++ b/src/ops/IdpOps.ts @@ -872,7 +872,6 @@ export async function importSocialIdentityProviders({ }): Promise { const response = []; const errors = []; - const imported = []; for (const idpId of Object.keys(importData.idp)) { try { if (options.deps && importData.idp[idpId].transform) { @@ -897,7 +896,6 @@ export async function importSocialIdentityProviders({ state, }) ); - imported.push(idpId); } catch (error) { errors.push(error); } @@ -905,9 +903,6 @@ export async function importSocialIdentityProviders({ if (errors.length > 0) { throw new FrodoError(`Error importing providers`); } - if (0 === imported.length) { - throw new FrodoError(`No providers found in import data!`); - } return response; } diff --git a/src/ops/JourneyOps.ts b/src/ops/JourneyOps.ts index 55086f38a..d2709a649 100644 --- a/src/ops/JourneyOps.ts +++ b/src/ops/JourneyOps.ts @@ -2344,7 +2344,6 @@ export async function importJourneys({ }): Promise { const response = []; const errors = []; - const imported = []; const installedJourneys = (await readJourneys({ state })).map((x) => x._id); const unresolvedJourneys: { [k: string]: string[]; @@ -2403,7 +2402,6 @@ export async function importJourneys({ state, }) ); - imported.push(tree); updateProgressIndicator({ id: indicatorId, message: `${tree}`, state }); } catch (error) { errors.push(error); @@ -2417,9 +2415,6 @@ export async function importJourneys({ }); throw new FrodoError(`Error importing journeys`, errors); } - if (0 === imported.length) { - throw new FrodoError(`No journeys found in import data!`); - } stopProgressIndicator({ id: indicatorId, message: 'Finished importing journeys', diff --git a/src/ops/MappingOps.ts b/src/ops/MappingOps.ts index a3671f416..829e03a17 100644 --- a/src/ops/MappingOps.ts +++ b/src/ops/MappingOps.ts @@ -937,7 +937,6 @@ export async function importMappings({ }): Promise { const response = []; const errors = []; - const imported = []; for (const key of Object.keys(importData.mapping)) { try { if (options.deps) { @@ -950,7 +949,6 @@ export async function importMappings({ state, }) ); - imported.push(key); } catch (error) { errors.push(error); } @@ -958,8 +956,5 @@ export async function importMappings({ if (errors.length > 0) { throw new FrodoError(`Error importing mappings`, errors); } - if (0 === imported.length) { - throw new FrodoError(`No mappings found in import data!`); - } return response; } diff --git a/src/ops/OAuth2ClientOps.ts b/src/ops/OAuth2ClientOps.ts index 4e9d0e26b..52d3da6f2 100644 --- a/src/ops/OAuth2ClientOps.ts +++ b/src/ops/OAuth2ClientOps.ts @@ -795,7 +795,6 @@ export async function importOAuth2Clients({ }): Promise { const response = []; const errors = []; - const imported = []; for (const id of Object.keys(importData.application)) { try { const clientData = importData.application[id]; @@ -807,16 +806,12 @@ export async function importOAuth2Clients({ response.push( await updateOAuth2Client({ clientId: id, clientData, state }) ); - imported.push(id); } catch (error) { errors.push(error); } } if (errors.length > 0) { - throw new FrodoError(`Error importing oauth2 clients`); - } - if (0 === imported.length) { - throw new FrodoError(`No oauth2 clients found in import data!`); + throw new FrodoError(`Error importing oauth2 clients`, errors); } return response; } diff --git a/src/ops/OAuth2TrustedJwtIssuerOps.ts b/src/ops/OAuth2TrustedJwtIssuerOps.ts index c860e0ec5..5cb9583d6 100644 --- a/src/ops/OAuth2TrustedJwtIssuerOps.ts +++ b/src/ops/OAuth2TrustedJwtIssuerOps.ts @@ -691,7 +691,6 @@ export async function importOAuth2TrustedJwtIssuers({ }): Promise { const response = []; const errors = []; - const imported = []; for (const id of Object.keys(importData.trustedJwtIssuer)) { try { const issuerData = importData.trustedJwtIssuer[id]; @@ -700,7 +699,6 @@ export async function importOAuth2TrustedJwtIssuers({ response.push( await updateOAuth2TrustedJwtIssuer({ issuerId: id, issuerData, state }) ); - imported.push(id); } catch (error) { errors.push(error); } @@ -708,8 +706,5 @@ export async function importOAuth2TrustedJwtIssuers({ if (errors.length > 0) { throw new FrodoError(`Error importing trusted issuers`, errors); } - if (0 === imported.length) { - throw new FrodoError(`No trusted issuers found in import data!`); - } return response; } diff --git a/src/ops/PolicyOps.ts b/src/ops/PolicyOps.ts index ddceb2f5a..d24c2718e 100644 --- a/src/ops/PolicyOps.ts +++ b/src/ops/PolicyOps.ts @@ -1180,7 +1180,6 @@ export async function importPolicies({ }): Promise { const response = []; const errors = []; - const imported = []; for (const id of Object.keys(importData.policy)) { try { const policyData = importData.policy[id]; @@ -1203,7 +1202,6 @@ export async function importPolicies({ response.push( await updatePolicy({ policyId: policyData._id, policyData, state }) ); - imported.push(id); } catch (error) { errors.push(error); } @@ -1225,8 +1223,5 @@ export async function importPolicies({ if (errors.length > 0) { throw new FrodoError(`Error importing policies`, errors); } - if (0 === imported.length) { - throw new FrodoError(`No policies found in import data`); - } return response; } diff --git a/src/ops/PolicySetOps.ts b/src/ops/PolicySetOps.ts index df59c1613..9600e8d2d 100644 --- a/src/ops/PolicySetOps.ts +++ b/src/ops/PolicySetOps.ts @@ -911,7 +911,6 @@ export async function importPolicySets({ }) { let response = null; const errors = []; - const imported = []; for (const id of Object.keys(importData.policyset)) { try { const policySetData = importData.policyset[id]; @@ -929,11 +928,9 @@ export async function importPolicySets({ } try { response = await _createPolicySet({ policySetData, state }); - imported.push(id); } catch (error) { if (error.response?.status === 409) { response = await _updatePolicySet({ policySetData, state }); - imported.push(id); } else throw error; } if (options.deps) { @@ -954,8 +951,5 @@ export async function importPolicySets({ if (errors.length > 0) { throw new FrodoError(`Error importing policy sets`, errors); } - if (0 === imported.length) { - throw new FrodoError(`No policy sets found in import data!`); - } return response; } diff --git a/src/ops/ResourceTypeOps.ts b/src/ops/ResourceTypeOps.ts index d7ff74979..1d4904e6c 100644 --- a/src/ops/ResourceTypeOps.ts +++ b/src/ops/ResourceTypeOps.ts @@ -704,7 +704,6 @@ export async function importResourceTypes({ }) { const response = []; const errors = []; - const imported = []; for (const id of Object.keys(importData.resourcetype)) { try { const resourceTypeData = importData.resourcetype[id]; @@ -722,7 +721,6 @@ export async function importResourceTypes({ ); else throw createError; } - imported.push(id); } catch (error) { errors.push(error); } @@ -730,9 +728,6 @@ export async function importResourceTypes({ if (errors.length > 0) { throw new FrodoError(`Error importing resource types`, errors); } - if (0 === imported.length) { - throw new FrodoError(`No resource types found in import data!`); - } return response; } diff --git a/src/ops/Saml2Ops.ts b/src/ops/Saml2Ops.ts index 87d120aea..ac0bd4352 100644 --- a/src/ops/Saml2Ops.ts +++ b/src/ops/Saml2Ops.ts @@ -13,12 +13,7 @@ import { } from '../api/Saml2Api'; import { getScript, type ScriptSkeleton } from '../api/ScriptApi'; import { State } from '../shared/State'; -import { - decode, - decodeBase64Url, - encode, - encodeBase64Url, -} from '../utils/Base64Utils'; +import { decodeBase64Url, encode, encodeBase64Url } from '../utils/Base64Utils'; import { createProgressIndicator, debugMessage, @@ -1031,7 +1026,6 @@ export async function importSaml2Providers({ debugMessage({ message: `Saml2Ops.importSaml2Providers: start`, state }); const response = []; const errors = []; - const imported = []; try { // find providers in hosted and in remote and map locations const hostedIds = Object.keys(importData.saml.hosted); @@ -1047,7 +1041,6 @@ export async function importSaml2Providers({ const location: Saml2ProiderLocation = hostedIds.includes(entityId64) ? 'hosted' : 'remote'; - const entityId = decode(entityId64); const providerData = importData.saml[location][entityId64]; if (options.deps) { try { @@ -1070,13 +1063,11 @@ export async function importSaml2Providers({ response.push( await _createProvider({ location, providerData, metaData, state }) ); - imported.push(entityId); } catch (createProviderErr) { try { response.push( await _updateProvider({ location, providerData, state }) ); - imported.push(entityId); } catch (error) { errors.push(error); } @@ -1092,9 +1083,6 @@ export async function importSaml2Providers({ } throw new FrodoError(`Error importing saml2 providers`, error); } - if (0 === imported.length) { - throw new Error(`No providers found in import data!`); - } debugMessage({ message: `Saml2Ops.importSaml2Providers: end`, state }); return response; } diff --git a/src/ops/ScriptOps.test.ts b/src/ops/ScriptOps.test.ts index 7c7385e44..f5b8ea598 100644 --- a/src/ops/ScriptOps.test.ts +++ b/src/ops/ScriptOps.test.ts @@ -515,18 +515,17 @@ describe('ScriptOps', () => { test(`2: Import script by name`, async () => { expect.assertions(1); - const outcome = await ScriptOps.importScripts({ + const result = await ScriptOps.importScripts({ scriptName: import1.name, importData: import1.data, state, }); - expect(outcome).toBeTruthy(); + expect(result).toMatchSnapshot(); }); test(`3: Import no scripts when excluding default scripts and only default scripts given`, async () => { expect.assertions(1); - try { - await ScriptOps.importScripts({ + const result = await ScriptOps.importScripts({ scriptName: '', importData: import2.data, options: { @@ -535,9 +534,7 @@ describe('ScriptOps', () => { }, state, }); - } catch (error) { - expect((error as FrodoError).getCombinedMessage()).toMatchSnapshot(); - } + expect(result).toMatchSnapshot(); }); }); diff --git a/src/ops/ScriptOps.ts b/src/ops/ScriptOps.ts index 41ab45cb9..4d719b9bb 100644 --- a/src/ops/ScriptOps.ts +++ b/src/ops/ScriptOps.ts @@ -611,7 +611,7 @@ export async function exportScripts({ * @param {ScriptExportInterface} importData Script import data * @param {ScriptImportOptions} options Script import options * @param {boolean} validate If true, validates Javascript scripts to ensure no errors exist in them. Default: false - * @returns {Promise} true if no errors occurred during import, false otherwise + * @returns {Promise} true if no errors occurred during import, false otherwise */ export async function importScripts({ scriptName, @@ -633,7 +633,6 @@ export async function importScripts({ try { debugMessage({ message: `ScriptOps.importScripts: start`, state }); const response = []; - const imported = []; for (const existingId of Object.keys(importData.script)) { try { const scriptData = importData.script[existingId]; @@ -663,12 +662,12 @@ export async function importScripts({ ); } } - await updateScript({ + const result = await updateScript({ scriptId: newId, scriptData, state, }); - imported.push(newId); + response.push(result); } catch (error) { errors.push(error); } @@ -676,9 +675,6 @@ export async function importScripts({ if (errors.length > 0) { throw new FrodoError(`Error importing scripts`, errors); } - if (0 === imported.length) { - throw new FrodoError(`No scripts found in import data`); - } debugMessage({ message: `ScriptOps.importScripts: end`, state }); return response; } catch (error) { diff --git a/src/ops/ServiceOps.ts b/src/ops/ServiceOps.ts index 48d6ab7fd..6ac725aa7 100644 --- a/src/ops/ServiceOps.ts +++ b/src/ops/ServiceOps.ts @@ -499,7 +499,7 @@ async function putFullServices({ }); } if (result) results.push(result); - printMessage({ message: `Imported: ${id}`, type: 'info', state }); + debugMessage({ message: `Imported: ${id}`, state }); } catch (error) { errors.push(error); } diff --git a/src/ops/cloud/AdminFederationOps.ts b/src/ops/cloud/AdminFederationOps.ts index 8d4b19a5f..05631565d 100644 --- a/src/ops/cloud/AdminFederationOps.ts +++ b/src/ops/cloud/AdminFederationOps.ts @@ -637,7 +637,6 @@ export async function importAdminFederationProviders({ }): Promise { const response = []; const errors = []; - const imported = []; for (const idpId of Object.keys(importData.idp)) { try { response.push( @@ -656,7 +655,6 @@ export async function importAdminFederationProviders({ state, }); } - imported.push(idpId); } catch (error) { errors.push(error); } @@ -664,8 +662,5 @@ export async function importAdminFederationProviders({ if (errors.length) { throw new FrodoError(`Error importing admin federation providers`, errors); } - if (0 === imported.length) { - throw new FrodoError(`No providers found in import data`); - } return response; } diff --git a/src/test/snapshots/ops/PolicyOps.test.js.snap b/src/test/snapshots/ops/PolicyOps.test.js.snap index 19bab0f5e..e1694009f 100644 --- a/src/test/snapshots/ops/PolicyOps.test.js.snap +++ b/src/test/snapshots/ops/PolicyOps.test.js.snap @@ -12,6 +12,7 @@ exports[`PolicyOps deletePolicy() 2: Delete non-existing policy [DoesNotExist] 1 HTTP client error Code: ERR_BAD_REQUEST Status: 404 + Reason: Not Found Message: Policy DoesNotExist does not exist." `; @@ -2787,6 +2788,7 @@ exports[`PolicyOps exportPolicy() 3: Export non-existing policy [DoesNotExist] 1 HTTP client error Code: ERR_BAD_REQUEST Status: 404 + Reason: Not Found Message: Policy DoesNotExist does not exist." `; @@ -4129,6 +4131,7 @@ exports[`PolicyOps readPolicy() 2: Get non-existing policy [DoesNotExist] 1`] = HTTP client error Code: ERR_BAD_REQUEST Status: 404 + Reason: Not Found Message: Policy DoesNotExist does not exist." `; diff --git a/src/test/snapshots/ops/PolicySetOps.test.js.snap b/src/test/snapshots/ops/PolicySetOps.test.js.snap index 6fbd24472..457723c67 100644 --- a/src/test/snapshots/ops/PolicySetOps.test.js.snap +++ b/src/test/snapshots/ops/PolicySetOps.test.js.snap @@ -59,6 +59,7 @@ exports[`PolicySetOps createPolicySet() 2: Create existing policy set [FrodoTest HTTP client error Code: ERR_BAD_REQUEST Status: 409 + Reason: Conflict Message: Application already exists" `; @@ -69,6 +70,7 @@ exports[`PolicySetOps deletePolicySet() 2: Delete non-existing policy set [DoesN HTTP client error Code: ERR_BAD_REQUEST Status: 404 + Reason: Not Found Message: DoesNotExist application not found in realm." `; @@ -201,6 +203,7 @@ exports[`PolicySetOps exportPolicySet() 3: Export non-existing policy set [DoesN HTTP client error Code: ERR_BAD_REQUEST Status: 404 + Reason: Not Found Message: Unable to retrieve application under realm /alpha." `; @@ -909,6 +912,7 @@ exports[`PolicySetOps readPolicySet() 2: Get non-existing policy set [DoesNotExi HTTP client error Code: ERR_BAD_REQUEST Status: 404 + Reason: Not Found Message: Unable to retrieve application under realm /alpha." `; @@ -1333,5 +1337,6 @@ exports[`PolicySetOps updatePolicySet() 2: Update non-existing policy set [Frodo HTTP client error Code: ERR_BAD_REQUEST Status: 404 + Reason: Not Found Message: FrodoTestPolicySet6 not found." `; diff --git a/src/test/snapshots/ops/ResourceTypeOps.test.js.snap b/src/test/snapshots/ops/ResourceTypeOps.test.js.snap index 539c854cb..f30338d6c 100644 --- a/src/test/snapshots/ops/ResourceTypeOps.test.js.snap +++ b/src/test/snapshots/ops/ResourceTypeOps.test.js.snap @@ -25,6 +25,7 @@ exports[`ResourceTypeOps createResourceType() 2: Create existing resource type [ HTTP client error Code: ERR_BAD_REQUEST Status: 409 + Reason: Conflict Message: Resource Type e219144d-8d94-47b6-8789-3a7b820d6cde already exists" `; @@ -35,6 +36,7 @@ exports[`ResourceTypeOps deleteResourceType() 2: Delete non-existing resource ty HTTP client error Code: ERR_BAD_REQUEST Status: 404 + Reason: Not Found Message: Resource Type 00000000-0000-0000-0000-000000000000 does not exist in realm /alpha" `; @@ -79,6 +81,7 @@ exports[`ResourceTypeOps exportResourceType() 2: Export non-existing resource ty HTTP client error Code: ERR_BAD_REQUEST Status: 404 + Reason: Not Found Message: Resource Type 00000000-0000-0000-0000-000000000000 does not exist in realm /alpha" `; @@ -532,6 +535,7 @@ exports[`ResourceTypeOps readResourceType() 2: Get non-existing resource type by HTTP client error Code: ERR_BAD_REQUEST Status: 404 + Reason: Not Found Message: Resource Type 00000000-0000-0000-0000-000000000000 does not exist in realm /alpha" `; @@ -850,5 +854,6 @@ exports[`ResourceTypeOps updateResourceType() 2: Update non-existing resource ty HTTP client error Code: ERR_BAD_REQUEST Status: 404 + Reason: Not Found Message: Resource Type 05e5fdb6-435e-43d9-b707-c73f7f347358 does not exist in realm /alpha" `; diff --git a/src/test/snapshots/ops/ScriptOps.test.js.snap b/src/test/snapshots/ops/ScriptOps.test.js.snap index f4a3164f9..524923a16 100644 --- a/src/test/snapshots/ops/ScriptOps.test.js.snap +++ b/src/test/snapshots/ops/ScriptOps.test.js.snap @@ -9334,11 +9334,27 @@ exports[`ScriptOps exportScripts() 2: Export all scripts, including default scri } `; -exports[`ScriptOps importScripts() 3: Import no scripts when excluding default scripts and only default scripts given 1`] = ` -"Error importing scripts - No scripts found in import data" +exports[`ScriptOps importScripts() 2: Import script by name 1`] = ` +[ + { + "_id": "5b3e4dd2-8060-4029-9ec1-6867932ab939", + "context": "AUTHENTICATION_TREE_DECISION_NODE", + "createdBy": "null", + "creationDate": 0, + "default": false, + "description": "Check if username has already been collected.", + "evaluatorVersion": "1.0", + "language": "JAVASCRIPT", + "lastModifiedBy": "id=76618ff6-e851-433e-9704-9d2852a17b7a,ou=user,ou=am-config", + "lastModifiedDate": 1704383306800, + "name": "FrodoTestScript5", + "script": "LyogQ2hlY2sgVXNlcm5hbWUKICoKICogQXV0aG9yOiB2b2xrZXIuc2NoZXViZXJAZm9yZ2Vyb2NrLmNvbQogKiAKICogQ2hlY2sgaWYgdXNlcm5hbWUgaGFzIGFscmVhZHkgYmVlbiBjb2xsZWN0ZWQuCiAqIFJldHVybiAia25vd24iIGlmIHllcywgInVua25vd24iIG90aGVyd2lzZS4KICogCiAqIFRoaXMgc2NyaXB0IGRvZXMgbm90IG5lZWQgdG8gYmUgcGFyYW1ldHJpemVkLiBJdCB3aWxsIHdvcmsgcHJvcGVybHkgYXMgaXMuCiAqIAogKiBUaGUgU2NyaXB0ZWQgRGVjaXNpb24gTm9kZSBuZWVkcyB0aGUgZm9sbG93aW5nIG91dGNvbWVzIGRlZmluZWQ6CiAqIC0ga25vd24KICogLSB1bmtub3duCiAqLwooZnVuY3Rpb24gKCkgewogICAgaWYgKG51bGwgIT0gc2hhcmVkU3RhdGUuZ2V0KCJ1c2VybmFtZSIpKSB7CiAgICAgICAgb3V0Y29tZSA9ICJrbm93biI7CiAgICB9CiAgICBlbHNlIHsKICAgICAgICBvdXRjb21lID0gInVua25vd24iOwogICAgfQp9KCkpOw==", + }, +] `; +exports[`ScriptOps importScripts() 3: Import no scripts when excluding default scripts and only default scripts given 1`] = `[]`; + exports[`ScriptOps readScript() 1: Read script by id 'c9cb4b1e-1cd3-4e5b-8f56-140f83ba9f6d' 1`] = ` { "_id": "c9cb4b1e-1cd3-4e5b-8f56-140f83ba9f6d", diff --git a/src/utils/ExportImportUtils.ts b/src/utils/ExportImportUtils.ts index 55af82a66..8707e7f72 100644 --- a/src/utils/ExportImportUtils.ts +++ b/src/utils/ExportImportUtils.ts @@ -14,7 +14,7 @@ import { encode, encodeBase64Url, } from './Base64Utils'; -import { debugMessage, printError, printMessage } from './Console'; +import { debugMessage, printMessage } from './Console'; import { deleteDeepByKey, stringify } from './JsonUtils'; export type ExportImport = { @@ -600,16 +600,17 @@ export function isValidUrl(urlString: string): boolean { export async function exportOrImportWithErrorHandling< P extends { state: State }, R, ->(func: (params: P) => Promise, parameters: P): Promise { +>( + func: (params: P) => Promise, + parameters: P, + errors: Error[] +): Promise { try { return await func(parameters); } catch (error) { - printError({ - error, - // eslint-disable-next-line prefer-rest-params - message: `Error in ${arguments[0]}`, - state: parameters.state, - }); + if (errors && Array.isArray(errors)) { + errors.push(error); + } return null; } }