From fbcb39d289462786d0c2207bb7c471c7c8b03121 Mon Sep 17 00:00:00 2001 From: Jason Lin Date: Wed, 26 Jul 2023 17:06:10 -0400 Subject: [PATCH 01/16] Adding new store functions and LD service to fetchPPP resolver. --- .../src/postgres/contractAndRates/index.ts | 31 +++++++++++++++++++ .../contractAndRates/prismaToDomainModel.ts | 8 +++-- .../app-api/src/postgres/postgresStore.ts | 23 ++++++++++++-- .../src/resolvers/configureResolvers.ts | 5 ++- .../app-api/src/testHelpers/storeHelpers.ts | 10 ++++++ 5 files changed, 71 insertions(+), 6 deletions(-) create mode 100644 services/app-api/src/postgres/contractAndRates/index.ts diff --git a/services/app-api/src/postgres/contractAndRates/index.ts b/services/app-api/src/postgres/contractAndRates/index.ts new file mode 100644 index 0000000000..a19f8dabd8 --- /dev/null +++ b/services/app-api/src/postgres/contractAndRates/index.ts @@ -0,0 +1,31 @@ +import { insertDraftContract, InsertContractArgsType } from './insertContract' +import { findContractWithHistory } from './findContractWithHistory' +import { findDraftContract } from './findDraftContract' +import { + contractFormDataToDomainModel, + convertUpdateInfoToDomainModel, + draftContractRevToDomainModel, + draftContractToDomainModel, + contractRevToDomainModel, + draftRatesToDomainModel, + ratesRevisionsToDomainModel, + contractWithHistoryToDomainModel, + getContractStatus, +} from './prismaToDomainModel' + +export { + insertDraftContract, + findContractWithHistory, + findDraftContract, + contractFormDataToDomainModel, + convertUpdateInfoToDomainModel, + draftContractRevToDomainModel, + draftContractToDomainModel, + contractRevToDomainModel, + draftRatesToDomainModel, + ratesRevisionsToDomainModel, + contractWithHistoryToDomainModel, + getContractStatus, +} + +export type { InsertContractArgsType } diff --git a/services/app-api/src/postgres/contractAndRates/prismaToDomainModel.ts b/services/app-api/src/postgres/contractAndRates/prismaToDomainModel.ts index ff75f9a27f..7a955cb56e 100644 --- a/services/app-api/src/postgres/contractAndRates/prismaToDomainModel.ts +++ b/services/app-api/src/postgres/contractAndRates/prismaToDomainModel.ts @@ -215,6 +215,9 @@ function contractWithHistoryToDomainModel( continue } + // This initial entry is the first history record of this contract revision. + // Then the for loop with it's rateRevisions are additional history records for each change in rate revisions. + // This is why allRevisionSets could have more entries than contract revisions. const initialEntry: ContractRevisionSet = { contractRev, submitInfo: contractRev.submitInfo, @@ -225,6 +228,7 @@ function contractWithHistoryToDomainModel( allRevisionSets.push(initialEntry) let lastEntry = initialEntry + // Now we construct a revision history for each change in rate revisions. // go through every rate revision in the join table in time order and construct a revisionSet // with (or without) the new rate revision in it. for (const rateRev of contractRev.rateRevisions) { @@ -270,11 +274,9 @@ function contractWithHistoryToDomainModel( const revisions = contractRevToDomainModel(allRevisionSets).reverse() - const contractStatus = getContractStatus(contract.revisions) - return { id: contract.id, - status: contractStatus, + status: getContractStatus(contract.revisions), stateCode: contract.stateCode, stateNumber: contract.stateNumber, revisions: revisions, diff --git a/services/app-api/src/postgres/postgresStore.ts b/services/app-api/src/postgres/postgresStore.ts index a5defc4649..34c3a0af6b 100644 --- a/services/app-api/src/postgres/postgresStore.ts +++ b/services/app-api/src/postgres/postgresStore.ts @@ -43,11 +43,16 @@ import { insertQuestionResponse, } from './questionResponse' import { findAllSupportedStates } from './state' -import { ContractType } from '../domain-models/contractAndRates/contractAndRatesZodSchema' +import { + ContractRevisionType, + ContractType, +} from '../domain-models/contractAndRates/contractAndRatesZodSchema' import { InsertContractArgsType, insertDraftContract, -} from './contractAndRates/insertContract' + findContractWithHistory, + findDraftContract, +} from './contractAndRates' type Store = { findPrograms: ( @@ -122,9 +127,20 @@ type Store = { user: StateUserType ) => Promise + /** + * Rates database refactor prisma functions + */ insertDraftContract: ( args: InsertContractArgsType ) => Promise + + findContractWithHistory: ( + contractID: string + ) => Promise + + findDraftContract: ( + contractID: string + ) => Promise } function NewPostgresStore(client: PrismaClient): Store { @@ -183,6 +199,9 @@ function NewPostgresStore(client: PrismaClient): Store { * Rates database refactor prisma functions */ insertDraftContract: (args) => insertDraftContract(client, args), + findContractWithHistory: (args) => + findContractWithHistory(client, args), + findDraftContract: (args) => findDraftContract(client, args), } } diff --git a/services/app-api/src/resolvers/configureResolvers.ts b/services/app-api/src/resolvers/configureResolvers.ts index 1f7f396fa6..91d0ee8f53 100644 --- a/services/app-api/src/resolvers/configureResolvers.ts +++ b/services/app-api/src/resolvers/configureResolvers.ts @@ -38,7 +38,10 @@ export function configureResolvers( DateTime: GraphQLDateTime, Query: { fetchCurrentUser: fetchCurrentUserResolver(), - fetchHealthPlanPackage: fetchHealthPlanPackageResolver(store), + fetchHealthPlanPackage: fetchHealthPlanPackageResolver( + store, + launchDarkly + ), indexHealthPlanPackages: indexHealthPlanPackagesResolver(store), indexUsers: indexUsersResolver(store), indexQuestions: indexQuestionsResolver(store), diff --git a/services/app-api/src/testHelpers/storeHelpers.ts b/services/app-api/src/testHelpers/storeHelpers.ts index fc3811c7a2..88a12605d2 100644 --- a/services/app-api/src/testHelpers/storeHelpers.ts +++ b/services/app-api/src/testHelpers/storeHelpers.ts @@ -101,6 +101,16 @@ function mockStoreThatErrors(): Store { 'UNEXPECTED_EXCEPTION: This error came from the generic store with errors mock' ) }, + findContractWithHistory: async (_ID) => { + return new Error( + 'UNEXPECTED_EXCEPTION: This error came from the generic store with errors mock' + ) + }, + findDraftContract: async (_ID) => { + return new Error( + 'UNEXPECTED_EXCEPTION: This error came from the generic store with errors mock' + ) + }, } } From 685302633c59910563ef616f3a79d67ac9d8dbcb Mon Sep 17 00:00:00 2001 From: Jason Lin Date: Wed, 26 Jul 2023 22:04:36 -0400 Subject: [PATCH 02/16] Add flag and logic for using new postgres functions. --- .../createHealthPlanPackage.ts | 4 +- .../fetchHealthPlanPackage.ts | 106 +++++++++++++++--- 2 files changed, 94 insertions(+), 16 deletions(-) diff --git a/services/app-api/src/resolvers/healthPlanPackage/createHealthPlanPackage.ts b/services/app-api/src/resolvers/healthPlanPackage/createHealthPlanPackage.ts index 10f5ebfcc2..37891d3b4c 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/createHealthPlanPackage.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/createHealthPlanPackage.ts @@ -113,9 +113,7 @@ export function createHealthPlanPackageResolver( logSuccess('createHealthPlanPackage') setSuccessAttributesOnActiveSpan(span) - return { - pkg, - } + return { pkg } } else { const pkgResult = await store.insertHealthPlanPackage(insertArgs) if (isStoreError(pkgResult)) { diff --git a/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts b/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts index 4955d41e7c..e6246450b2 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts @@ -5,35 +5,117 @@ import { isAdminUser, HealthPlanPackageType, packageStatus, + convertContractToUnlockedHealthPlanPackage, } from '../../domain-models' import { isHelpdeskUser } from '../../domain-models/user' import { QueryResolvers, State } from '../../gen/gqlServer' import { logError, logSuccess } from '../../logger' -import { isStoreError, Store } from '../../postgres' +import { isStoreError, Store, StoreError } from '../../postgres' import { setErrorAttributesOnActiveSpan, setResolverDetailsOnActiveSpan, setSuccessAttributesOnActiveSpan, } from '../attributeHelper' +import { LDService } from '../../launchDarkly/launchDarkly' +import { GraphQLError } from 'graphql/index' export function fetchHealthPlanPackageResolver( - store: Store + store: Store, + launchDarkly: LDService ): QueryResolvers['fetchHealthPlanPackage'] { return async (_parent, { input }, context) => { const { user, span } = context setResolverDetailsOnActiveSpan('fetchHealthPlanPackage', user, span) - // fetch from the store - const result = await store.findHealthPlanPackage(input.pkgID) - if (isStoreError(result)) { - const errMessage = `Issue finding a package of type ${result.code}. Message: ${result.message}` - logError('fetchHealthPlanPackage', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new Error(errMessage) + const ratesDatabaseRefactor = await launchDarkly.getFeatureFlag( + context, + 'rates-db-refactor' + ) + + let pkg: HealthPlanPackageType = {} + + // Here is where we flag finding health plan + if (ratesDatabaseRefactor) { + // Health plans can be in two states Draft and Submitted, and we have 2 postgres functions for each. + // findContractWithHistory gets all submitted revisions and findDraftContract gets just the one draft revision + // We don't have function that gets all revisions including draft. So we have to call both functions when a + // contract is DRAFT. + const contractWithHistory = await store.findContractWithHistory( + input.pkgID + ) + + if (contractWithHistory instanceof Error) { + const errMessage = `Issue finding a package with id ${input.pkgID}. Message: ${contractWithHistory.message}` + logError('fetchHealthPlanPackage', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new GraphQLError(errMessage, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'DB_ERROR', + }, + }) + } + + if (contractWithHistory.status === 'DRAFT') { + const draftRevision = await store.findDraftContract(input.pkgID) + if (draftRevision instanceof Error) { + // If draft returns undefined we error because a draft submission should always have a draft revision. + const errMessage = `Issue finding a package with id ${input.pkgID}. Message: ${draftRevision.message}` + logError('fetchHealthPlanPackage', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new GraphQLError(errMessage, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'DB_ERROR', + }, + }) + } + + if (draftRevision === undefined) { + const errMessage = `Issue finding a package with id ${input.pkgID}. Message: Result was undefined.` + logError('fetchHealthPlanPackage', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new GraphQLError(errMessage, { + extensions: { code: 'NOT_FOUND' }, + }) + } + + // Pushing in the draft revision, so it would be first in the array of revisions. + contractWithHistory.revisions.push(draftRevision) + } + + const convertedPkg = + convertContractToUnlockedHealthPlanPackage(contractWithHistory) + + if (convertedPkg instanceof Error) { + const errMessage = `Issue converting contract. Message: ${convertedPkg.message}` + logError('fetchHealthPlanPackage', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new GraphQLError(errMessage, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'PROTO_DECODE_ERROR', + }, + }) + } + + pkg = convertedPkg + } else { + const result = (await store.findHealthPlanPackage(input.pkgID)) as + | HealthPlanPackageType + | StoreError + if (isStoreError(result)) { + const errMessage = `Issue finding a package of type ${result.code}. Message: ${result.message}` + logError('fetchHealthPlanPackage', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new Error(errMessage) + } + + pkg = result } - if (result === undefined) { - const errMessage = `Issue finding a package with id ${input.pkgID}. Message: Result was undefined ` + if (pkg === undefined) { + const errMessage = `Issue finding a package with id ${input.pkgID}. Message: Result was undefined.` logError('fetchHealthPlanPackage', errMessage) setErrorAttributesOnActiveSpan(errMessage, span) return { @@ -41,8 +123,6 @@ export function fetchHealthPlanPackageResolver( } } - const pkg: HealthPlanPackageType = result - // Authorization CMS users can view, state users can only view if the state matches if (isStateUser(context.user)) { const stateFromCurrentUser: State['code'] = context.user.stateCode From 68b3844a8a96e34f2af3be1078cc6c9ef82e4be6 Mon Sep 17 00:00:00 2001 From: Jason Lin Date: Wed, 26 Jul 2023 23:01:43 -0400 Subject: [PATCH 03/16] Throw an error when contract or hpp is not found instead of undefined. --- .../resolvers/healthPlanPackage/fetchHealthPlanPackage.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts b/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts index e6246450b2..d75f498880 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts @@ -118,9 +118,9 @@ export function fetchHealthPlanPackageResolver( const errMessage = `Issue finding a package with id ${input.pkgID}. Message: Result was undefined.` logError('fetchHealthPlanPackage', errMessage) setErrorAttributesOnActiveSpan(errMessage, span) - return { - pkg: undefined, - } + throw new GraphQLError(errMessage, { + extensions: { code: 'NOT_FOUND' }, + }) } // Authorization CMS users can view, state users can only view if the state matches From 65800ccdc65daaa8d1dcbfecc7e8c7ab711be667 Mon Sep 17 00:00:00 2001 From: Jason Lin Date: Wed, 26 Jul 2023 23:02:53 -0400 Subject: [PATCH 04/16] Update graphQL schema to make pkg not null. --- services/app-graphql/src/schema.graphql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/app-graphql/src/schema.graphql b/services/app-graphql/src/schema.graphql index e61739561b..4740511a71 100644 --- a/services/app-graphql/src/schema.graphql +++ b/services/app-graphql/src/schema.graphql @@ -264,7 +264,7 @@ input FetchHealthPlanPackageInput { type FetchHealthPlanPackagePayload { "A single HealthPlanPackage" - pkg: HealthPlanPackage + pkg: HealthPlanPackage! } type HealthPlanPackageEdge { From 8d05ac3e3dd9bbddf0ef1e33629a58c37d9d0623 Mon Sep 17 00:00:00 2001 From: Jason Lin Date: Wed, 26 Jul 2023 23:03:26 -0400 Subject: [PATCH 05/16] Update frontend and backend tests for not found error being thrown. --- .../fetchHealthPlanPackage.test.ts | 96 ++++++++++++++++++- .../StateSubmissionForm.test.tsx | 2 +- .../StateSubmission/StateSubmissionForm.tsx | 13 ++- .../SubmissionRevisionSummary.tsx | 13 ++- .../SubmissionSideNav.test.tsx | 2 +- .../SubmissionSideNav/SubmissionSideNav.tsx | 12 +-- .../apolloMocks/healthPlanPackageGQLMock.ts | 15 ++- .../apolloMocks/questionResponseGQLMock.ts | 16 +++- 8 files changed, 133 insertions(+), 36 deletions(-) diff --git a/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.test.ts b/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.test.ts index e5318d77e8..03067e7e58 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.test.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.test.ts @@ -9,6 +9,7 @@ import { resubmitTestHealthPlanPackage, } from '../../testHelpers/gqlHelpers' import { testCMSUser, testStateUser } from '../../testHelpers/userHelpers' +import { testLDService } from '../../testHelpers/launchDarklyHelpers' describe('fetchHealthPlanPackage', () => { const testUserCMS = testCMSUser() @@ -48,6 +49,10 @@ describe('fetchHealthPlanPackage', () => { throw subData } + // When not using tables, the protobuf ID is used to as the HPP id when inserting a new HPP in the tables. + // So HPP id and proto id are the same. + // Now that our form data is in postgres contract revision table, the ids are not the same. So this expect is + // removed when flag is on. expect(subData.id).toEqual(createdID) expect(subData.programIDs).toEqual([ '5c10fe9f-bec9-416f-a20c-718b152ad633', @@ -63,7 +68,7 @@ describe('fetchHealthPlanPackage', () => { ]) }) - it('returns nothing if the ID doesnt exist', async () => { + it('returns error if the ID doesnt exist', async () => { const server = await constructTestPostgresServer() // then see if we can fetch that same submission @@ -76,10 +81,17 @@ describe('fetchHealthPlanPackage', () => { variables: { input }, }) - expect(result.errors).toBeUndefined() + expect(result.errors).toBeDefined() + if (result.errors === undefined) { + throw new Error('annoying jest typing behavior') + } + expect(result.errors).toHaveLength(1) + const resultErr = result.errors[0] - const resultSub = result.data?.fetchHealthPlanPackage.pkg - expect(resultSub).toBeNull() + expect(resultErr?.message).toBe( + `Issue finding a package with id ${input.pkgID}. Message: Result was undefined.` + ) + expect(resultErr?.extensions?.code).toBe('NOT_FOUND') }) it('returns multiple submissions payload with multiple revisions', async () => { @@ -377,3 +389,79 @@ describe('fetchHealthPlanPackage', () => { } }) }) + +// Currently we cannot set up fetchHPP tests like createHPP because not all resolvers have been migrated yet. +// Once all resolvers are migrated and tests in this describe block mirrors the ones above, we can then use describe.each +describe('fetchHealthPlanPackage rates-db-refactor flag on tests', () => { + const mockLDService = testLDService({ 'rates-db-refactor': true }) + it('returns package with one revision', async () => { + const server = await constructTestPostgresServer({ + ldService: mockLDService, + }) + + // First, create a new submission + const stateSubmission = await createTestHealthPlanPackage(server) + + const createdID = stateSubmission.id + + // then see if we can fetch that same submission + const input = { + pkgID: createdID, + } + + const result = await server.executeOperation({ + query: FETCH_HEALTH_PLAN_PACKAGE, + variables: { input }, + }) + + expect(result.errors).toBeUndefined() + + const resultSub = result.data?.fetchHealthPlanPackage.pkg + expect(resultSub.id).toEqual(createdID) + expect(resultSub.revisions).toHaveLength(1) + + const revision = resultSub.revisions[0].node + + const subData = base64ToDomain(revision.formDataProto) + if (subData instanceof Error) { + throw subData + } + + // Expect the created revision and the fetchHPP revision are the same. + expect(subData.id).toEqual(stateSubmission.revisions[0].node.id) + + expect(subData.programIDs).toEqual([ + '5c10fe9f-bec9-416f-a20c-718b152ad633', + ]) + expect(subData.submissionDescription).toBe('A created submission') + expect(subData.documents).toEqual([]) + }) + + it('returns error if the ID doesnt exist', async () => { + const server = await constructTestPostgresServer({ + ldService: mockLDService, + }) + + // then see if we can fetch that same submission + const input = { + pkgID: 'BOGUS-ID', + } + + const result = await server.executeOperation({ + query: FETCH_HEALTH_PLAN_PACKAGE, + variables: { input }, + }) + + expect(result.errors).toBeDefined() + if (result.errors === undefined) { + throw new Error('annoying jest typing behavior') + } + expect(result.errors).toHaveLength(1) + const resultErr = result.errors[0] + + expect(resultErr?.message).toBe( + `Issue finding a package with id ${input.pkgID}. Message: PRISMA ERROR: Cannot find contract with id: BOGUS-ID` + ) + expect(resultErr?.extensions?.code).toBe('INTERNAL_SERVER_ERROR') + }) +}) diff --git a/services/app-web/src/pages/StateSubmission/StateSubmissionForm.test.tsx b/services/app-web/src/pages/StateSubmission/StateSubmissionForm.test.tsx index ec58313c78..f029eca5b0 100644 --- a/services/app-web/src/pages/StateSubmission/StateSubmissionForm.test.tsx +++ b/services/app-web/src/pages/StateSubmission/StateSubmissionForm.test.tsx @@ -488,7 +488,7 @@ describe('StateSubmissionForm', () => { }) }) - it('shows a generic 404 page when package is undefined', async () => { + it('shows a generic 404 page when package is not found', async () => { renderWithProviders( { if (fetchResult.status === 'ERROR') { const err = fetchResult.error console.error('Error from API fetch', fetchResult.error) + if (err instanceof ApolloError) { handleApolloError(err, true) - } else { - recordJSException(err) + if (err.graphQLErrors[0]?.extensions?.code === 'NOT_FOUND') { + return + } } + + recordJSException(err) return // api failure or protobuf decode failure } const { data, revisionsLookup } = fetchResult const pkg = data.fetchHealthPlanPackage.pkg - // fetchHPP returns null if no package is found with the given ID - if (!pkg) { - return - } - // pull out the latest revision and document lookups const latestRevision = pkg.revisions[0].node const formDataFromLatestRevision = diff --git a/services/app-web/src/pages/SubmissionRevisionSummary/SubmissionRevisionSummary.tsx b/services/app-web/src/pages/SubmissionRevisionSummary/SubmissionRevisionSummary.tsx index 691e7a1645..3ce975b62b 100644 --- a/services/app-web/src/pages/SubmissionRevisionSummary/SubmissionRevisionSummary.tsx +++ b/services/app-web/src/pages/SubmissionRevisionSummary/SubmissionRevisionSummary.tsx @@ -54,20 +54,19 @@ export const SubmissionRevisionSummary = (): React.ReactElement => { console.error('Error from API fetch', fetchResult.error) if (err instanceof ApolloError) { handleApolloError(err, true) - } else { - recordJSException(err) + + if (err.graphQLErrors[0]?.extensions?.code === 'NOT_FOUND') { + return + } } + + recordJSException(err) return // api failure or protobuf decode failure } const { data, revisionsLookup, documentDates } = fetchResult const pkg = data.fetchHealthPlanPackage.pkg - // fetchHPP returns null if no package is found with the given ID - if (!pkg) { - return - } - //We offset version by +1 of index, remove offset to find revision in revisions const revisionIndex = Number(revisionVersion) - 1 //Reversing revisions to get correct submission order diff --git a/services/app-web/src/pages/SubmissionSideNav/SubmissionSideNav.test.tsx b/services/app-web/src/pages/SubmissionSideNav/SubmissionSideNav.test.tsx index b6b3860627..ac9b7e42b4 100644 --- a/services/app-web/src/pages/SubmissionSideNav/SubmissionSideNav.test.tsx +++ b/services/app-web/src/pages/SubmissionSideNav/SubmissionSideNav.test.tsx @@ -493,7 +493,7 @@ describe('SubmissionSideNav', () => { expect(await screen.findByText('System error')).toBeInTheDocument() }) - it('shows a generic 404 page when package is undefined', async () => { + it('shows a generic 404 page when package is not found', async () => { renderWithProviders( }> diff --git a/services/app-web/src/pages/SubmissionSideNav/SubmissionSideNav.tsx b/services/app-web/src/pages/SubmissionSideNav/SubmissionSideNav.tsx index cbcd0eae30..aac7193e37 100644 --- a/services/app-web/src/pages/SubmissionSideNav/SubmissionSideNav.tsx +++ b/services/app-web/src/pages/SubmissionSideNav/SubmissionSideNav.tsx @@ -75,9 +75,13 @@ export const SubmissionSideNav = () => { console.error('Error from API fetch', fetchResult.error) if (err instanceof ApolloError) { handleApolloError(err, true) - } else { - recordJSException(err) + + if (err.graphQLErrors[0]?.extensions?.code === 'NOT_FOUND') { + return + } } + + recordJSException(err) return // api failure or protobuf decode failure } @@ -97,10 +101,6 @@ export const SubmissionSideNav = () => { return } - // fetchHPP with questions returns null if no package or questions is found with the given ID - if (!pkg) { - return - } const submissionStatus = pkg.status const isCMSUser = loggedInUser?.role === 'CMS_USER' diff --git a/services/app-web/src/testHelpers/apolloMocks/healthPlanPackageGQLMock.ts b/services/app-web/src/testHelpers/apolloMocks/healthPlanPackageGQLMock.ts index 60f2ec88b8..9702f12d68 100644 --- a/services/app-web/src/testHelpers/apolloMocks/healthPlanPackageGQLMock.ts +++ b/services/app-web/src/testHelpers/apolloMocks/healthPlanPackageGQLMock.ts @@ -63,17 +63,22 @@ const fetchHealthPlanPackageMockSuccess = ({ const fetchHealthPlanPackageMockNotFound = ({ id, }: fetchHealthPlanPackageMockProps): MockedResponse => { + const graphQLError = new GraphQLError( + 'Issue finding a package with id a6039ed6-39cc-4814-8eaa-0c99f25e325d. Message: Result was undefined.', + { + extensions: { + code: 'NOT_FOUND', + }, + } + ) + return { request: { query: FetchHealthPlanPackageDocument, variables: { input: { pkgID: id } }, }, result: { - data: { - fetchHealthPlanPackage: { - pkg: undefined, - }, - }, + errors: [graphQLError], }, } } diff --git a/services/app-web/src/testHelpers/apolloMocks/questionResponseGQLMock.ts b/services/app-web/src/testHelpers/apolloMocks/questionResponseGQLMock.ts index 326f8a9613..797985f20d 100644 --- a/services/app-web/src/testHelpers/apolloMocks/questionResponseGQLMock.ts +++ b/services/app-web/src/testHelpers/apolloMocks/questionResponseGQLMock.ts @@ -13,6 +13,7 @@ import { } from '../../gen/gqlClient' import { mockValidCMSUser } from './userGQLMock' import { mockSubmittedHealthPlanPackage, mockQuestionsPayload } from './' +import { GraphQLError } from 'graphql' type fetchStateHealthPlanPackageWithQuestionsProps = { stateSubmission?: HealthPlanPackage | Partial @@ -109,17 +110,22 @@ const fetchStateHealthPlanPackageWithQuestionsMockSuccess = ({ const fetchStateHealthPlanPackageWithQuestionsMockNotFound = ({ id, }: fetchStateHealthPlanPackageWithQuestionsProps): MockedResponse => { + const graphQLError = new GraphQLError( + 'Issue finding a package with id a6039ed6-39cc-4814-8eaa-0c99f25e325d. Message: Result was undefined.', + { + extensions: { + code: 'NOT_FOUND', + }, + } + ) + return { request: { query: FetchHealthPlanPackageWithQuestionsDocument, variables: { input: { pkgID: id } }, }, result: { - data: { - fetchHealthPlanPackage: { - pkg: undefined, - }, - }, + errors: [graphQLError], }, } } From 51709f01bdc5a5d7087e64b718b1e415344bc55f Mon Sep 17 00:00:00 2001 From: Jason Lin Date: Thu, 27 Jul 2023 00:46:20 -0400 Subject: [PATCH 06/16] Add `NOT_FOUND_ERROR` to storeErrors and update new db functions to return store errors. --- .../findContractWithHistory.ts | 13 ++++--------- .../contractAndRates/submitContract.ts | 13 +++++-------- .../contractAndRates/unlockContract.ts | 18 +++++------------- .../contractAndRates/updateDraftContract.ts | 11 ++++------- services/app-api/src/postgres/postgresStore.ts | 2 +- services/app-api/src/postgres/storeError.ts | 8 ++++---- 6 files changed, 23 insertions(+), 42 deletions(-) diff --git a/services/app-api/src/postgres/contractAndRates/findContractWithHistory.ts b/services/app-api/src/postgres/contractAndRates/findContractWithHistory.ts index 3aa0e1934b..0c92fe9309 100644 --- a/services/app-api/src/postgres/contractAndRates/findContractWithHistory.ts +++ b/services/app-api/src/postgres/contractAndRates/findContractWithHistory.ts @@ -2,6 +2,7 @@ import { PrismaTransactionType } from '../prismaTypes' import { ContractType } from '../../domain-models/contractAndRates/contractAndRatesZodSchema' import { parseContractWithHistory } from '../../domain-models/contractAndRates/parseDomainData' import { updateInfoIncludeUpdater } from '../prismaHelpers' +import { convertPrismaErrorToStoreError, StoreError } from '../storeError' // findContractWithHistory returns a ContractType with a full set of // ContractRevisions in reverse chronological order. Each revision is a change to this @@ -10,9 +11,9 @@ import { updateInfoIncludeUpdater } from '../prismaHelpers' async function findContractWithHistory( client: PrismaTransactionType, contractID: string -): Promise { +): Promise { try { - const contract = await client.contractTable.findFirst({ + const contract = await client.contractTable.findFirstOrThrow({ where: { id: contractID, }, @@ -45,16 +46,10 @@ async function findContractWithHistory( }, }) - if (!contract) { - const err = `PRISMA ERROR: Cannot find contract with id: ${contractID}` - console.error(err) - return new Error(err) - } - return parseContractWithHistory(contract) } catch (err) { console.error('PRISMA ERROR', err) - return err + return convertPrismaErrorToStoreError(err) } } diff --git a/services/app-api/src/postgres/contractAndRates/submitContract.ts b/services/app-api/src/postgres/contractAndRates/submitContract.ts index 9987204109..ad6d314f65 100644 --- a/services/app-api/src/postgres/contractAndRates/submitContract.ts +++ b/services/app-api/src/postgres/contractAndRates/submitContract.ts @@ -1,6 +1,7 @@ import { PrismaClient } from '@prisma/client' import { ContractType } from '../../domain-models/contractAndRates/contractAndRatesZodSchema' import { findContractWithHistory } from './findContractWithHistory' +import { StoreError, convertPrismaErrorToStoreError } from '../storeError' // Update the given revision // * invalidate relationships of previous revision @@ -10,14 +11,14 @@ async function submitContract( contractID: string, submittedByUserID: string, submitReason: string -): Promise { +): Promise { const groupTime = new Date() try { return await client.$transaction(async (tx) => { // Given all the Rates associated with this draft, find the most recent submitted // rateRevision to attach to this contract on submit. - const currentRev = await tx.contractRevisionTable.findFirst({ + const currentRev = await tx.contractRevisionTable.findFirstOrThrow({ where: { contractID: contractID, submitInfoID: null, @@ -38,10 +39,6 @@ async function submitContract( }, }, }) - if (!currentRev) { - console.error('No Unsubmitted Rev!') - return new Error('cant find the current rev to submit') - } const submittedRateRevisions = currentRev.draftRates.map( (c) => c.revisions[0] @@ -149,8 +146,8 @@ async function submitContract( return await findContractWithHistory(tx, contractID) }) } catch (err) { - console.error('SUBMITeeee PRISMA CONTRACT ERR', err) - return err + console.error('PRISMA CONTRACT ERR', err) + return convertPrismaErrorToStoreError(err) } } diff --git a/services/app-api/src/postgres/contractAndRates/unlockContract.ts b/services/app-api/src/postgres/contractAndRates/unlockContract.ts index 928d0dfc03..e67e5fa007 100644 --- a/services/app-api/src/postgres/contractAndRates/unlockContract.ts +++ b/services/app-api/src/postgres/contractAndRates/unlockContract.ts @@ -1,6 +1,7 @@ import { PrismaClient } from '@prisma/client' import { ContractType } from '../../domain-models/contractAndRates/contractAndRatesZodSchema' import { findContractWithHistory } from './findContractWithHistory' +import { StoreError, convertPrismaErrorToStoreError } from '../storeError' // Unlock the given contract // * copy form data @@ -10,14 +11,14 @@ async function unlockContract( contractID: string, unlockedByUserID: string, unlockReason: string -): Promise { +): Promise { const groupTime = new Date() try { return await client.$transaction(async (tx) => { // Given all the Rates associated with this draft, find the most recent submitted // rateRevision to attach to this contract on submit. - const currentRev = await tx.contractRevisionTable.findFirst({ + const currentRev = await tx.contractRevisionTable.findFirstOrThrow({ where: { contractID: contractID, }, @@ -35,15 +36,6 @@ async function unlockContract( }, }, }) - if (!currentRev) { - console.error( - 'Programming Error: cannot find the current revision to submit' - ) - return new Error( - 'Programming Error: cannot find the current revision to submit' - ) - } - if (!currentRev.submitInfoID) { console.error( 'Programming Error: cannot unlock a already unlocked contract' @@ -95,8 +87,8 @@ async function unlockContract( return findContractWithHistory(tx, contractID) }) } catch (err) { - console.error('SUBMIT PRISMA CONTRACT ERR', err) - return err + console.error('PRISMA CONTRACT ERR', err) + return convertPrismaErrorToStoreError(err) } } diff --git a/services/app-api/src/postgres/contractAndRates/updateDraftContract.ts b/services/app-api/src/postgres/contractAndRates/updateDraftContract.ts index 73b20ebb71..8528fef6e9 100644 --- a/services/app-api/src/postgres/contractAndRates/updateDraftContract.ts +++ b/services/app-api/src/postgres/contractAndRates/updateDraftContract.ts @@ -6,6 +6,7 @@ import { } from '@prisma/client' import { ContractType } from '../../domain-models/contractAndRates/contractAndRatesZodSchema' import { findContractWithHistory } from './findContractWithHistory' +import { StoreError, convertPrismaErrorToStoreError } from '../storeError' type UpdateContractArgsType = { populationCovered: PopulationCoverageType @@ -24,20 +25,16 @@ async function updateDraftContract( contractID: string, formData: UpdateContractArgsType, rateIDs: string[] -): Promise { +): Promise { try { // Given all the Rates associated with this draft, find the most recent submitted // rateRevision to update. - const currentRev = await client.contractRevisionTable.findFirst({ + const currentRev = await client.contractRevisionTable.findFirstOrThrow({ where: { contractID: contractID, submitInfoID: null, }, }) - if (!currentRev) { - console.error('No Draft Rev!') - return new Error('cant find a draft rev to submit') - } await client.contractRevisionTable.update({ where: { @@ -68,7 +65,7 @@ async function updateDraftContract( return findContractWithHistory(client, contractID) } catch (err) { console.error('SUBMIT PRISMA CONTRACT ERR', err) - return err + return convertPrismaErrorToStoreError(err) } } diff --git a/services/app-api/src/postgres/postgresStore.ts b/services/app-api/src/postgres/postgresStore.ts index 34c3a0af6b..636a39e7b4 100644 --- a/services/app-api/src/postgres/postgresStore.ts +++ b/services/app-api/src/postgres/postgresStore.ts @@ -136,7 +136,7 @@ type Store = { findContractWithHistory: ( contractID: string - ) => Promise + ) => Promise findDraftContract: ( contractID: string diff --git a/services/app-api/src/postgres/storeError.ts b/services/app-api/src/postgres/storeError.ts index 543fb05fd7..9ba5e46dab 100644 --- a/services/app-api/src/postgres/storeError.ts +++ b/services/app-api/src/postgres/storeError.ts @@ -11,6 +11,7 @@ const StoreErrorCodes = [ 'USER_FORMAT_ERROR', 'UNEXPECTED_EXCEPTION', 'WRONG_STATUS', + 'NOT_FOUND_ERROR', ] as const type StoreErrorCode = (typeof StoreErrorCodes)[number] // iterable union type @@ -53,12 +54,11 @@ const convertPrismaErrorToStoreError = (prismaErr: unknown): StoreError => { // An operation failed because it depends on one or more records // that were required but not found. - // This is also returned when the userID doesn't exist in trying to connect - // a user and some states if (prismaErr.code === 'P2025') { return { - code: 'INSERT_ERROR', - message: 'insert failed because required record not found', + code: 'NOT_FOUND_ERROR', + message: + 'An operation failed because it depends on one or more records that were required but not found.', } } From 7a00d148b4bef639c17ebd85bc2c1a4b48f4b363 Mon Sep 17 00:00:00 2001 From: Jason Lin Date: Thu, 27 Jul 2023 00:49:00 -0400 Subject: [PATCH 07/16] Throw error graphQL error for new db function storeErrors. --- .../healthPlanPackage/fetchHealthPlanPackage.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts b/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts index d75f498880..5369c24dbd 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts @@ -44,6 +44,20 @@ export function fetchHealthPlanPackageResolver( input.pkgID ) + if (isStoreError(contractWithHistory)) { + const errMessage = `Issue finding a package with id ${input.pkgID}. Message: ${contractWithHistory.message}` + logError('fetchHealthPlanPackage', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + + if (contractWithHistory?.code === 'NOT_FOUND_ERROR') { + throw new GraphQLError(errMessage, { + extensions: { code: 'NOT_FOUND' }, + }) + } + + throw new Error(errMessage) + } + if (contractWithHistory instanceof Error) { const errMessage = `Issue finding a package with id ${input.pkgID}. Message: ${contractWithHistory.message}` logError('fetchHealthPlanPackage', errMessage) From 92e18190d68e716652a4bca387a900cd8a1946aa Mon Sep 17 00:00:00 2001 From: Jason Lin Date: Thu, 27 Jul 2023 00:50:46 -0400 Subject: [PATCH 08/16] Throw error graphQL error for new db function storeErrors. --- .../findContractWithHistory.test.ts | 42 +++++++++---------- .../contractAndRates/submitContract.test.ts | 41 ++++++++++++------ .../fetchHealthPlanPackage.test.ts | 4 +- .../createQuestionResponse.test.ts | 2 +- .../src/resolvers/user/updateCMSUser.ts | 2 +- .../app-api/src/testHelpers/errorHelpers.ts | 8 +++- 6 files changed, 58 insertions(+), 41 deletions(-) diff --git a/services/app-api/src/postgres/contractAndRates/findContractWithHistory.test.ts b/services/app-api/src/postgres/contractAndRates/findContractWithHistory.test.ts index 5601b40fc6..2196e9cbe2 100644 --- a/services/app-api/src/postgres/contractAndRates/findContractWithHistory.test.ts +++ b/services/app-api/src/postgres/contractAndRates/findContractWithHistory.test.ts @@ -11,6 +11,7 @@ import { updateDraftRate } from './updateDraftRate' import { unlockRate } from './unlockRate' import { findRateWithHistory } from './findRateWithHistory' import { must, createInsertContractData } from '../../testHelpers' +import { isStoreError } from '../storeError' describe('findContract', () => { it('finds a stripped down contract with history', async () => { @@ -92,9 +93,8 @@ describe('findContract', () => { must(await submitRate(client, rate3.id, stateUser.id, '3.0 create')) // Now, find that contract and assert the history is what we expected - const threeContract = await findContractWithHistory( - client, - contractA.id + const threeContract = must( + await findContractWithHistory(client, contractA.id) ) if (threeContract instanceof Error) { throw threeContract @@ -114,7 +114,9 @@ describe('findContract', () => { must(await submitRate(client, rate2.id, stateUser.id, '2.1 remove')) // Now, find that contract and assert the history is what we expected - const twoContract = await findContractWithHistory(client, contractA.id) + const twoContract = must( + await findContractWithHistory(client, contractA.id) + ) if (twoContract instanceof Error) { throw twoContract } @@ -131,9 +133,8 @@ describe('findContract', () => { must(await submitRate(client, rate1.id, stateUser.id, '1.1 new name')) // Now, find that contract and assert the history is what we expected - const backAgainContract = await findContractWithHistory( - client, - contractA.id + const backAgainContract = must( + await findContractWithHistory(client, contractA.id) ) if (backAgainContract instanceof Error) { throw backAgainContract @@ -159,9 +160,8 @@ describe('findContract', () => { ) // Now, find that contract and assert the history is what we expected - let testingContract = await findContractWithHistory( - client, - contractA.id + let testingContract = must( + await findContractWithHistory(client, contractA.id) ) if (testingContract instanceof Error) { throw testingContract @@ -202,16 +202,17 @@ describe('findContract', () => { ) // Now, find that contract and assert the history is what we expected - testingContract = await findContractWithHistory(client, contractA.id) + testingContract = must( + await findContractWithHistory(client, contractA.id) + ) if (testingContract instanceof Error) { throw testingContract } expect(testingContract.revisions).toHaveLength(8) // Now, find that contract and assert the history is what we expected - const resultingContract = await findContractWithHistory( - client, - contractA.id + const resultingContract = must( + await findContractWithHistory(client, contractA.id) ) if (resultingContract instanceof Error) { throw resultingContract @@ -453,9 +454,8 @@ describe('findContract', () => { ) // Now, find that contract and assert the history is what we expected - const resultingContract = await findContractWithHistory( - client, - contractA.id + const resultingContract = must( + await findContractWithHistory(client, contractA.id) ) if (resultingContract instanceof Error) { throw resultingContract @@ -624,15 +624,11 @@ describe('findContract', () => { stateUser.id, 'third submit' ) - if (!(contractA_1_Error instanceof Error)) { + if (!isStoreError(contractA_1_Error)) { throw new Error('Should be impossible to submit twice in a row.') } - const res = await findContractWithHistory(client, contractA.id) - - if (res instanceof Error) { - throw res - } + const res = must(await findContractWithHistory(client, contractA.id)) const revisions = res.revisions.reverse() diff --git a/services/app-api/src/postgres/contractAndRates/submitContract.test.ts b/services/app-api/src/postgres/contractAndRates/submitContract.test.ts index b4688f87eb..2f23e0d86d 100644 --- a/services/app-api/src/postgres/contractAndRates/submitContract.test.ts +++ b/services/app-api/src/postgres/contractAndRates/submitContract.test.ts @@ -6,6 +6,7 @@ import { insertDraftRate } from './insertRate' import { submitRate } from './submitRate' import { updateDraftRate } from './updateDraftRate' import { must, createInsertContractData } from '../../testHelpers' +import { isStoreError } from '../storeError' describe('submitContract', () => { it('creates a submission from a draft', async () => { @@ -22,10 +23,19 @@ describe('submitContract', () => { }, }) - // submitting before there's a draft should be an error - expect( - await submitContract(client, '1111', '1111', 'failed submit') - ).toBeInstanceOf(Error) + // submitting before there's a draft should be a store error + const storeError = await submitContract( + client, + '1111', + '1111', + 'failed submit' + ) + expect(isStoreError(storeError)).toBeTruthy() + expect(storeError).toEqual( + expect.objectContaining({ + code: 'NOT_FOUND_ERROR', + }) + ) // create a draft contract const draftContractData = createInsertContractData({ @@ -61,15 +71,20 @@ describe('submitContract', () => { }) ) - // resubmitting should be an error - expect( - await submitContract( - client, - contractA.id, - stateUser.id, - 'initial submit' - ) - ).toBeInstanceOf(Error) + const resubmitStoreError = await submitContract( + client, + contractA.id, + stateUser.id, + 'initial submit' + ) + + // resubmitting should be a store error + expect(isStoreError(resubmitStoreError)).toBeTruthy() + expect(resubmitStoreError).toEqual( + expect.objectContaining({ + code: 'NOT_FOUND_ERROR', + }) + ) }) it('invalidates old revisions when new revisions are submitted', async () => { diff --git a/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.test.ts b/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.test.ts index 03067e7e58..3f30174c2d 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.test.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.test.ts @@ -460,8 +460,8 @@ describe('fetchHealthPlanPackage rates-db-refactor flag on tests', () => { const resultErr = result.errors[0] expect(resultErr?.message).toBe( - `Issue finding a package with id ${input.pkgID}. Message: PRISMA ERROR: Cannot find contract with id: BOGUS-ID` + `Issue finding a package with id ${input.pkgID}. Message: An operation failed because it depends on one or more records that were required but not found.` ) - expect(resultErr?.extensions?.code).toBe('INTERNAL_SERVER_ERROR') + expect(resultErr?.extensions?.code).toBe('NOT_FOUND') }) }) diff --git a/services/app-api/src/resolvers/questionResponse/createQuestionResponse.test.ts b/services/app-api/src/resolvers/questionResponse/createQuestionResponse.test.ts index abd4eee846..a82028ba8d 100644 --- a/services/app-api/src/resolvers/questionResponse/createQuestionResponse.test.ts +++ b/services/app-api/src/resolvers/questionResponse/createQuestionResponse.test.ts @@ -79,7 +79,7 @@ describe('createQuestionResponse', () => { expect(createdResponse.errors).toBeDefined() expect(assertAnErrorCode(createdResponse)).toBe('BAD_USER_INPUT') expect(assertAnError(createdResponse).message).toBe( - `Issue creating question response for question ${fakeID} of type INSERT_ERROR. Message: insert failed because required record not found` + `Issue creating question response for question ${fakeID} of type NOT_FOUND_ERROR. Message: An operation failed because it depends on one or more records that were required but not found.` ) }) diff --git a/services/app-api/src/resolvers/user/updateCMSUser.ts b/services/app-api/src/resolvers/user/updateCMSUser.ts index f819483e01..23c59c977f 100644 --- a/services/app-api/src/resolvers/user/updateCMSUser.ts +++ b/services/app-api/src/resolvers/user/updateCMSUser.ts @@ -106,7 +106,7 @@ export function updateCMSUserResolver( 'Updated user assignments' // someday might have a note field and make this a param ) if (isStoreError(result)) { - if (result.code === 'INSERT_ERROR') { + if (result.code === 'NOT_FOUND_ERROR') { const errMsg = 'cmsUserID does not exist' logError('updateCmsUser', errMsg) setErrorAttributesOnActiveSpan(errMsg, span) diff --git a/services/app-api/src/testHelpers/errorHelpers.ts b/services/app-api/src/testHelpers/errorHelpers.ts index 30275a9b56..505d93ac1a 100644 --- a/services/app-api/src/testHelpers/errorHelpers.ts +++ b/services/app-api/src/testHelpers/errorHelpers.ts @@ -1,8 +1,14 @@ // For use in TESTS only. Throws a returned error -function must(maybeErr: T | Error): T { +import { isStoreError, StoreError } from '../postgres' + +function must(maybeErr: T | Error | StoreError): T { if (maybeErr instanceof Error) { throw maybeErr } + + if (isStoreError(maybeErr)) { + throw maybeErr + } return maybeErr } From af924738819475622d0bbbfb687a8a66654f06f5 Mon Sep 17 00:00:00 2001 From: Jason Lin Date: Thu, 27 Jul 2023 00:53:58 -0400 Subject: [PATCH 09/16] Fix bug with testLDService saving flag values. --- services/app-api/src/testHelpers/launchDarklyHelpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/app-api/src/testHelpers/launchDarklyHelpers.ts b/services/app-api/src/testHelpers/launchDarklyHelpers.ts index 10e7228288..1f42d723c8 100644 --- a/services/app-api/src/testHelpers/launchDarklyHelpers.ts +++ b/services/app-api/src/testHelpers/launchDarklyHelpers.ts @@ -7,7 +7,7 @@ import { import { defaultFeatureFlags } from '../launchDarkly/launchDarkly' function testLDService(mockFeatureFlags?: FeatureFlagSettings): LDService { - const featureFlags = defaultFeatureFlags + const featureFlags = Object.assign({}, defaultFeatureFlags) //Update featureFlags with mock flag values. if (mockFeatureFlags) { From 6afbb06210ca194b318d86be8f7bb5e8705f42c3 Mon Sep 17 00:00:00 2001 From: Jason Lin Date: Thu, 27 Jul 2023 11:39:23 -0400 Subject: [PATCH 10/16] Fix error messages. --- .../healthPlanPackage/fetchHealthPlanPackage.test.ts | 2 +- .../resolvers/healthPlanPackage/fetchHealthPlanPackage.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.test.ts b/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.test.ts index 3f30174c2d..6781e7f796 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.test.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.test.ts @@ -460,7 +460,7 @@ describe('fetchHealthPlanPackage rates-db-refactor flag on tests', () => { const resultErr = result.errors[0] expect(resultErr?.message).toBe( - `Issue finding a package with id ${input.pkgID}. Message: An operation failed because it depends on one or more records that were required but not found.` + `Issue finding a contract with history with id ${input.pkgID}. Message: An operation failed because it depends on one or more records that were required but not found.` ) expect(resultErr?.extensions?.code).toBe('NOT_FOUND') }) diff --git a/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts b/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts index 5369c24dbd..fc39daf049 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts @@ -45,7 +45,7 @@ export function fetchHealthPlanPackageResolver( ) if (isStoreError(contractWithHistory)) { - const errMessage = `Issue finding a package with id ${input.pkgID}. Message: ${contractWithHistory.message}` + const errMessage = `Issue finding a contract with history with id ${input.pkgID}. Message: ${contractWithHistory.message}` logError('fetchHealthPlanPackage', errMessage) setErrorAttributesOnActiveSpan(errMessage, span) @@ -59,7 +59,7 @@ export function fetchHealthPlanPackageResolver( } if (contractWithHistory instanceof Error) { - const errMessage = `Issue finding a package with id ${input.pkgID}. Message: ${contractWithHistory.message}` + const errMessage = `Issue finding a contract with history with id ${input.pkgID}. Message: ${contractWithHistory.message}` logError('fetchHealthPlanPackage', errMessage) setErrorAttributesOnActiveSpan(errMessage, span) throw new GraphQLError(errMessage, { @@ -74,7 +74,7 @@ export function fetchHealthPlanPackageResolver( const draftRevision = await store.findDraftContract(input.pkgID) if (draftRevision instanceof Error) { // If draft returns undefined we error because a draft submission should always have a draft revision. - const errMessage = `Issue finding a package with id ${input.pkgID}. Message: ${draftRevision.message}` + const errMessage = `Issue finding a draft contract with id ${input.pkgID}. Message: ${draftRevision.message}` logError('fetchHealthPlanPackage', errMessage) setErrorAttributesOnActiveSpan(errMessage, span) throw new GraphQLError(errMessage, { @@ -86,7 +86,7 @@ export function fetchHealthPlanPackageResolver( } if (draftRevision === undefined) { - const errMessage = `Issue finding a package with id ${input.pkgID}. Message: Result was undefined.` + const errMessage = `Issue finding a draft contract with id ${input.pkgID}. Message: Result was undefined.` logError('fetchHealthPlanPackage', errMessage) setErrorAttributesOnActiveSpan(errMessage, span) throw new GraphQLError(errMessage, { From 6a1ccf36bc4109ef73b56a3e7c553afed3f8deaa Mon Sep 17 00:00:00 2001 From: Jason Lin Date: Thu, 27 Jul 2023 12:42:43 -0400 Subject: [PATCH 11/16] Unify graphQL errors. --- .../fetchHealthPlanPackage.ts | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts b/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts index fc39daf049..a563f975f2 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts @@ -51,11 +51,19 @@ export function fetchHealthPlanPackageResolver( if (contractWithHistory?.code === 'NOT_FOUND_ERROR') { throw new GraphQLError(errMessage, { - extensions: { code: 'NOT_FOUND' }, + extensions: { + code: 'NOT_FOUND', + cause: 'DB_ERROR', + }, }) } - throw new Error(errMessage) + throw new GraphQLError(errMessage, { + extensions: { + code: 'INTERNAL_SERVER_ERROR', + cause: 'DB_ERROR', + }, + }) } if (contractWithHistory instanceof Error) { @@ -90,7 +98,10 @@ export function fetchHealthPlanPackageResolver( logError('fetchHealthPlanPackage', errMessage) setErrorAttributesOnActiveSpan(errMessage, span) throw new GraphQLError(errMessage, { - extensions: { code: 'NOT_FOUND' }, + extensions: { + code: 'NOT_FOUND', + cause: 'DB_ERROR', + }, }) } @@ -125,16 +136,19 @@ export function fetchHealthPlanPackageResolver( throw new Error(errMessage) } - pkg = result - } + if (result === undefined) { + const errMessage = `Issue finding a package with id ${input.pkgID}. Message: Result was undefined.` + logError('fetchHealthPlanPackage', errMessage) + setErrorAttributesOnActiveSpan(errMessage, span) + throw new GraphQLError(errMessage, { + extensions: { + code: 'NOT_FOUND', + cause: 'DB_ERROR', + }, + }) + } - if (pkg === undefined) { - const errMessage = `Issue finding a package with id ${input.pkgID}. Message: Result was undefined.` - logError('fetchHealthPlanPackage', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new GraphQLError(errMessage, { - extensions: { code: 'NOT_FOUND' }, - }) + pkg = result } // Authorization CMS users can view, state users can only view if the state matches From 548256d56f67a9d5cfa68cd1cc86438b32f658c5 Mon Sep 17 00:00:00 2001 From: Jason Lin Date: Fri, 28 Jul 2023 12:16:48 -0400 Subject: [PATCH 12/16] Remove type assertions. --- .../healthPlanPackage/fetchHealthPlanPackage.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts b/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts index a563f975f2..867fa9cc94 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts @@ -10,7 +10,7 @@ import { import { isHelpdeskUser } from '../../domain-models/user' import { QueryResolvers, State } from '../../gen/gqlServer' import { logError, logSuccess } from '../../logger' -import { isStoreError, Store, StoreError } from '../../postgres' +import { isStoreError, Store } from '../../postgres' import { setErrorAttributesOnActiveSpan, setResolverDetailsOnActiveSpan, @@ -32,7 +32,7 @@ export function fetchHealthPlanPackageResolver( 'rates-db-refactor' ) - let pkg: HealthPlanPackageType = {} + let pkg: HealthPlanPackageType // Here is where we flag finding health plan if (ratesDatabaseRefactor) { @@ -126,9 +126,8 @@ export function fetchHealthPlanPackageResolver( pkg = convertedPkg } else { - const result = (await store.findHealthPlanPackage(input.pkgID)) as - | HealthPlanPackageType - | StoreError + const result = await store.findHealthPlanPackage(input.pkgID) + if (isStoreError(result)) { const errMessage = `Issue finding a package of type ${result.code}. Message: ${result.message}` logError('fetchHealthPlanPackage', errMessage) From 9cd5c100f24346b2aab6d8b737120eeb316b6b10 Mon Sep 17 00:00:00 2001 From: Jason Lin Date: Fri, 28 Jul 2023 12:17:05 -0400 Subject: [PATCH 13/16] Change defaultFeatureFlags to a function. --- services/app-api/src/launchDarkly/launchDarkly.ts | 11 +++++------ .../app-api/src/testHelpers/launchDarklyHelpers.ts | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/services/app-api/src/launchDarkly/launchDarkly.ts b/services/app-api/src/launchDarkly/launchDarkly.ts index 2a871c23ec..2e645b85f0 100644 --- a/services/app-api/src/launchDarkly/launchDarkly.ts +++ b/services/app-api/src/launchDarkly/launchDarkly.ts @@ -11,14 +11,12 @@ import { logError } from '../logger' import { setErrorAttributesOnActiveSpan } from '../resolvers/attributeHelper' //Set up default feature flag values used to returned data -const defaultFeatureFlags: FeatureFlagSettings = featureFlagKeys.reduce( - (a, c) => { +const defaultFeatureFlags = (): FeatureFlagSettings => + featureFlagKeys.reduce((a, c) => { const flag = featureFlags[c].flag const defaultValue = featureFlags[c].defaultValue return Object.assign(a, { [flag]: defaultValue }) - }, - {} as FeatureFlagSettings -) + }, {} as FeatureFlagSettings) type LDService = { getFeatureFlag: ( @@ -50,7 +48,8 @@ function offlineLDService(): LDService { `No connection to LaunchDarkly, fallback to offlineLDService with default value for ${flag}`, context.span ) - return defaultFeatureFlags[flag] + const featureFlags = defaultFeatureFlags() + return featureFlags[flag] }, } } diff --git a/services/app-api/src/testHelpers/launchDarklyHelpers.ts b/services/app-api/src/testHelpers/launchDarklyHelpers.ts index 1f42d723c8..2d7b4d497f 100644 --- a/services/app-api/src/testHelpers/launchDarklyHelpers.ts +++ b/services/app-api/src/testHelpers/launchDarklyHelpers.ts @@ -7,7 +7,7 @@ import { import { defaultFeatureFlags } from '../launchDarkly/launchDarkly' function testLDService(mockFeatureFlags?: FeatureFlagSettings): LDService { - const featureFlags = Object.assign({}, defaultFeatureFlags) + const featureFlags = defaultFeatureFlags() //Update featureFlags with mock flag values. if (mockFeatureFlags) { From bfd32ea24a27bae97d59bc89b7f7281a4b2e8a89 Mon Sep 17 00:00:00 2001 From: Jason Lin Date: Tue, 1 Aug 2023 15:25:24 -0400 Subject: [PATCH 14/16] Change to custom errors and fix tests. --- services/app-api/src/errors/errors.ts | 9 +++++++++ services/app-api/src/errors/index.ts | 3 +++ .../findContractWithHistory.test.ts | 3 +-- .../findContractWithHistory.ts | 14 +++++++++---- .../contractAndRates/submitContract.test.ts | 20 +++++-------------- .../contractAndRates/submitContract.ts | 16 ++++++++++----- .../contractAndRates/unlockContract.ts | 16 ++++++++++----- .../contractAndRates/updateDraftContract.ts | 15 +++++++++----- .../app-api/src/postgres/postgresStore.ts | 2 +- .../fetchHealthPlanPackage.test.ts | 2 +- .../fetchHealthPlanPackage.ts | 17 +++------------- 11 files changed, 65 insertions(+), 52 deletions(-) create mode 100644 services/app-api/src/errors/errors.ts create mode 100644 services/app-api/src/errors/index.ts diff --git a/services/app-api/src/errors/errors.ts b/services/app-api/src/errors/errors.ts new file mode 100644 index 0000000000..07c36da372 --- /dev/null +++ b/services/app-api/src/errors/errors.ts @@ -0,0 +1,9 @@ +class NotFoundError extends Error { + constructor(message: string) { + super(message) + + Object.setPrototypeOf(this, NotFoundError.prototype) + } +} + +export { NotFoundError } diff --git a/services/app-api/src/errors/index.ts b/services/app-api/src/errors/index.ts new file mode 100644 index 0000000000..d6ae37e101 --- /dev/null +++ b/services/app-api/src/errors/index.ts @@ -0,0 +1,3 @@ +import { NotFoundError } from './errors' + +export { NotFoundError } diff --git a/services/app-api/src/postgres/contractAndRates/findContractWithHistory.test.ts b/services/app-api/src/postgres/contractAndRates/findContractWithHistory.test.ts index 2196e9cbe2..c01208f53d 100644 --- a/services/app-api/src/postgres/contractAndRates/findContractWithHistory.test.ts +++ b/services/app-api/src/postgres/contractAndRates/findContractWithHistory.test.ts @@ -11,7 +11,6 @@ import { updateDraftRate } from './updateDraftRate' import { unlockRate } from './unlockRate' import { findRateWithHistory } from './findRateWithHistory' import { must, createInsertContractData } from '../../testHelpers' -import { isStoreError } from '../storeError' describe('findContract', () => { it('finds a stripped down contract with history', async () => { @@ -624,7 +623,7 @@ describe('findContract', () => { stateUser.id, 'third submit' ) - if (!isStoreError(contractA_1_Error)) { + if (!(contractA_1_Error instanceof Error)) { throw new Error('Should be impossible to submit twice in a row.') } diff --git a/services/app-api/src/postgres/contractAndRates/findContractWithHistory.ts b/services/app-api/src/postgres/contractAndRates/findContractWithHistory.ts index 0c92fe9309..1198bca3b8 100644 --- a/services/app-api/src/postgres/contractAndRates/findContractWithHistory.ts +++ b/services/app-api/src/postgres/contractAndRates/findContractWithHistory.ts @@ -2,7 +2,7 @@ import { PrismaTransactionType } from '../prismaTypes' import { ContractType } from '../../domain-models/contractAndRates/contractAndRatesZodSchema' import { parseContractWithHistory } from '../../domain-models/contractAndRates/parseDomainData' import { updateInfoIncludeUpdater } from '../prismaHelpers' -import { convertPrismaErrorToStoreError, StoreError } from '../storeError' +import { NotFoundError } from '../../errors' // findContractWithHistory returns a ContractType with a full set of // ContractRevisions in reverse chronological order. Each revision is a change to this @@ -11,9 +11,9 @@ import { convertPrismaErrorToStoreError, StoreError } from '../storeError' async function findContractWithHistory( client: PrismaTransactionType, contractID: string -): Promise { +): Promise { try { - const contract = await client.contractTable.findFirstOrThrow({ + const contract = await client.contractTable.findFirst({ where: { id: contractID, }, @@ -46,10 +46,16 @@ async function findContractWithHistory( }, }) + if (!contract) { + const err = `PRISMA ERROR: Cannot find contract with id: ${contractID}` + console.error(err) + return new NotFoundError(err) + } + return parseContractWithHistory(contract) } catch (err) { console.error('PRISMA ERROR', err) - return convertPrismaErrorToStoreError(err) + return err } } diff --git a/services/app-api/src/postgres/contractAndRates/submitContract.test.ts b/services/app-api/src/postgres/contractAndRates/submitContract.test.ts index 2f23e0d86d..e2670d3af9 100644 --- a/services/app-api/src/postgres/contractAndRates/submitContract.test.ts +++ b/services/app-api/src/postgres/contractAndRates/submitContract.test.ts @@ -6,7 +6,7 @@ import { insertDraftRate } from './insertRate' import { submitRate } from './submitRate' import { updateDraftRate } from './updateDraftRate' import { must, createInsertContractData } from '../../testHelpers' -import { isStoreError } from '../storeError' +import { NotFoundError } from '../../errors' describe('submitContract', () => { it('creates a submission from a draft', async () => { @@ -23,19 +23,14 @@ describe('submitContract', () => { }, }) - // submitting before there's a draft should be a store error - const storeError = await submitContract( + // submitting before there's a draft should be an error + const submitError = await submitContract( client, '1111', '1111', 'failed submit' ) - expect(isStoreError(storeError)).toBeTruthy() - expect(storeError).toEqual( - expect.objectContaining({ - code: 'NOT_FOUND_ERROR', - }) - ) + expect(submitError).toBeInstanceOf(NotFoundError) // create a draft contract const draftContractData = createInsertContractData({ @@ -79,12 +74,7 @@ describe('submitContract', () => { ) // resubmitting should be a store error - expect(isStoreError(resubmitStoreError)).toBeTruthy() - expect(resubmitStoreError).toEqual( - expect.objectContaining({ - code: 'NOT_FOUND_ERROR', - }) - ) + expect(resubmitStoreError).toBeInstanceOf(NotFoundError) }) it('invalidates old revisions when new revisions are submitted', async () => { diff --git a/services/app-api/src/postgres/contractAndRates/submitContract.ts b/services/app-api/src/postgres/contractAndRates/submitContract.ts index ad6d314f65..65d165d375 100644 --- a/services/app-api/src/postgres/contractAndRates/submitContract.ts +++ b/services/app-api/src/postgres/contractAndRates/submitContract.ts @@ -1,7 +1,7 @@ import { PrismaClient } from '@prisma/client' import { ContractType } from '../../domain-models/contractAndRates/contractAndRatesZodSchema' import { findContractWithHistory } from './findContractWithHistory' -import { StoreError, convertPrismaErrorToStoreError } from '../storeError' +import { NotFoundError } from '../../errors' // Update the given revision // * invalidate relationships of previous revision @@ -11,14 +11,14 @@ async function submitContract( contractID: string, submittedByUserID: string, submitReason: string -): Promise { +): Promise { const groupTime = new Date() try { return await client.$transaction(async (tx) => { // Given all the Rates associated with this draft, find the most recent submitted // rateRevision to attach to this contract on submit. - const currentRev = await tx.contractRevisionTable.findFirstOrThrow({ + const currentRev = await tx.contractRevisionTable.findFirst({ where: { contractID: contractID, submitInfoID: null, @@ -40,6 +40,12 @@ async function submitContract( }, }) + if (!currentRev) { + const err = `PRISMA ERROR: Cannot find the current rev to submit with contract id: ${contractID}` + console.error(err) + return new NotFoundError(err) + } + const submittedRateRevisions = currentRev.draftRates.map( (c) => c.revisions[0] ) @@ -146,8 +152,8 @@ async function submitContract( return await findContractWithHistory(tx, contractID) }) } catch (err) { - console.error('PRISMA CONTRACT ERR', err) - return convertPrismaErrorToStoreError(err) + console.error('SUBMIT PRISMA CONTRACT ERR', err) + return err } } diff --git a/services/app-api/src/postgres/contractAndRates/unlockContract.ts b/services/app-api/src/postgres/contractAndRates/unlockContract.ts index e67e5fa007..cc2c2c2552 100644 --- a/services/app-api/src/postgres/contractAndRates/unlockContract.ts +++ b/services/app-api/src/postgres/contractAndRates/unlockContract.ts @@ -1,7 +1,7 @@ import { PrismaClient } from '@prisma/client' import { ContractType } from '../../domain-models/contractAndRates/contractAndRatesZodSchema' import { findContractWithHistory } from './findContractWithHistory' -import { StoreError, convertPrismaErrorToStoreError } from '../storeError' +import { NotFoundError } from '../../errors' // Unlock the given contract // * copy form data @@ -11,14 +11,14 @@ async function unlockContract( contractID: string, unlockedByUserID: string, unlockReason: string -): Promise { +): Promise { const groupTime = new Date() try { return await client.$transaction(async (tx) => { // Given all the Rates associated with this draft, find the most recent submitted // rateRevision to attach to this contract on submit. - const currentRev = await tx.contractRevisionTable.findFirstOrThrow({ + const currentRev = await tx.contractRevisionTable.findFirst({ where: { contractID: contractID, }, @@ -36,6 +36,12 @@ async function unlockContract( }, }, }) + if (!currentRev) { + const err = `PRISMA ERROR: Cannot find the current revision to unlock with contract id: ${contractID}` + console.error(err) + return new NotFoundError(err) + } + if (!currentRev.submitInfoID) { console.error( 'Programming Error: cannot unlock a already unlocked contract' @@ -87,8 +93,8 @@ async function unlockContract( return findContractWithHistory(tx, contractID) }) } catch (err) { - console.error('PRISMA CONTRACT ERR', err) - return convertPrismaErrorToStoreError(err) + console.error('UNLOCK PRISMA CONTRACT ERR', err) + return err } } diff --git a/services/app-api/src/postgres/contractAndRates/updateDraftContract.ts b/services/app-api/src/postgres/contractAndRates/updateDraftContract.ts index 8528fef6e9..82094d45de 100644 --- a/services/app-api/src/postgres/contractAndRates/updateDraftContract.ts +++ b/services/app-api/src/postgres/contractAndRates/updateDraftContract.ts @@ -6,7 +6,7 @@ import { } from '@prisma/client' import { ContractType } from '../../domain-models/contractAndRates/contractAndRatesZodSchema' import { findContractWithHistory } from './findContractWithHistory' -import { StoreError, convertPrismaErrorToStoreError } from '../storeError' +import { NotFoundError } from '../../errors' type UpdateContractArgsType = { populationCovered: PopulationCoverageType @@ -25,16 +25,21 @@ async function updateDraftContract( contractID: string, formData: UpdateContractArgsType, rateIDs: string[] -): Promise { +): Promise { try { // Given all the Rates associated with this draft, find the most recent submitted // rateRevision to update. - const currentRev = await client.contractRevisionTable.findFirstOrThrow({ + const currentRev = await client.contractRevisionTable.findFirst({ where: { contractID: contractID, submitInfoID: null, }, }) + if (!currentRev) { + const err = `PRISMA ERROR: Cannot find the current rev to update with contract id: ${contractID}` + console.error(err) + return new NotFoundError(err) + } await client.contractRevisionTable.update({ where: { @@ -64,8 +69,8 @@ async function updateDraftContract( return findContractWithHistory(client, contractID) } catch (err) { - console.error('SUBMIT PRISMA CONTRACT ERR', err) - return convertPrismaErrorToStoreError(err) + console.error('UPDATE PRISMA CONTRACT ERR', err) + return err } } diff --git a/services/app-api/src/postgres/postgresStore.ts b/services/app-api/src/postgres/postgresStore.ts index 636a39e7b4..34c3a0af6b 100644 --- a/services/app-api/src/postgres/postgresStore.ts +++ b/services/app-api/src/postgres/postgresStore.ts @@ -136,7 +136,7 @@ type Store = { findContractWithHistory: ( contractID: string - ) => Promise + ) => Promise findDraftContract: ( contractID: string diff --git a/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.test.ts b/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.test.ts index 6781e7f796..480123c9c2 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.test.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.test.ts @@ -460,7 +460,7 @@ describe('fetchHealthPlanPackage rates-db-refactor flag on tests', () => { const resultErr = result.errors[0] expect(resultErr?.message).toBe( - `Issue finding a contract with history with id ${input.pkgID}. Message: An operation failed because it depends on one or more records that were required but not found.` + `Issue finding a contract with history with id ${input.pkgID}. Message: PRISMA ERROR: Cannot find contract with id: BOGUS-ID` ) expect(resultErr?.extensions?.code).toBe('NOT_FOUND') }) diff --git a/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts b/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts index 867fa9cc94..a13dadfb91 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts @@ -18,6 +18,7 @@ import { } from '../attributeHelper' import { LDService } from '../../launchDarkly/launchDarkly' import { GraphQLError } from 'graphql/index' +import { NotFoundError } from '../../errors' export function fetchHealthPlanPackageResolver( store: Store, @@ -44,12 +45,12 @@ export function fetchHealthPlanPackageResolver( input.pkgID ) - if (isStoreError(contractWithHistory)) { + if (contractWithHistory instanceof Error) { const errMessage = `Issue finding a contract with history with id ${input.pkgID}. Message: ${contractWithHistory.message}` logError('fetchHealthPlanPackage', errMessage) setErrorAttributesOnActiveSpan(errMessage, span) - if (contractWithHistory?.code === 'NOT_FOUND_ERROR') { + if (contractWithHistory instanceof NotFoundError) { throw new GraphQLError(errMessage, { extensions: { code: 'NOT_FOUND', @@ -66,18 +67,6 @@ export function fetchHealthPlanPackageResolver( }) } - if (contractWithHistory instanceof Error) { - const errMessage = `Issue finding a contract with history with id ${input.pkgID}. Message: ${contractWithHistory.message}` - logError('fetchHealthPlanPackage', errMessage) - setErrorAttributesOnActiveSpan(errMessage, span) - throw new GraphQLError(errMessage, { - extensions: { - code: 'INTERNAL_SERVER_ERROR', - cause: 'DB_ERROR', - }, - }) - } - if (contractWithHistory.status === 'DRAFT') { const draftRevision = await store.findDraftContract(input.pkgID) if (draftRevision instanceof Error) { From ec104c63177316039596bcaaa553495ff9a6fd38 Mon Sep 17 00:00:00 2001 From: Jason Lin Date: Wed, 2 Aug 2023 22:52:28 -0400 Subject: [PATCH 15/16] Move NotFoundError to postgres directory. --- services/app-api/src/errors/errors.ts | 9 --------- services/app-api/src/errors/index.ts | 3 --- .../contractAndRates/findContractWithHistory.ts | 4 ++-- .../postgres/contractAndRates/submitContract.ts | 4 ++-- .../postgres/contractAndRates/unlockContract.ts | 4 ++-- .../contractAndRates/updateDraftContract.ts | 4 ++-- services/app-api/src/postgres/index.ts | 2 +- services/app-api/src/postgres/storeError.ts | 15 ++++++++++++++- .../healthPlanPackage/fetchHealthPlanPackage.ts | 2 +- 9 files changed, 24 insertions(+), 23 deletions(-) delete mode 100644 services/app-api/src/errors/errors.ts delete mode 100644 services/app-api/src/errors/index.ts diff --git a/services/app-api/src/errors/errors.ts b/services/app-api/src/errors/errors.ts deleted file mode 100644 index 07c36da372..0000000000 --- a/services/app-api/src/errors/errors.ts +++ /dev/null @@ -1,9 +0,0 @@ -class NotFoundError extends Error { - constructor(message: string) { - super(message) - - Object.setPrototypeOf(this, NotFoundError.prototype) - } -} - -export { NotFoundError } diff --git a/services/app-api/src/errors/index.ts b/services/app-api/src/errors/index.ts deleted file mode 100644 index d6ae37e101..0000000000 --- a/services/app-api/src/errors/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { NotFoundError } from './errors' - -export { NotFoundError } diff --git a/services/app-api/src/postgres/contractAndRates/findContractWithHistory.ts b/services/app-api/src/postgres/contractAndRates/findContractWithHistory.ts index 1198bca3b8..bb916a44b3 100644 --- a/services/app-api/src/postgres/contractAndRates/findContractWithHistory.ts +++ b/services/app-api/src/postgres/contractAndRates/findContractWithHistory.ts @@ -2,7 +2,7 @@ import { PrismaTransactionType } from '../prismaTypes' import { ContractType } from '../../domain-models/contractAndRates/contractAndRatesZodSchema' import { parseContractWithHistory } from '../../domain-models/contractAndRates/parseDomainData' import { updateInfoIncludeUpdater } from '../prismaHelpers' -import { NotFoundError } from '../../errors' +import { NotFoundError } from '../storeError' // findContractWithHistory returns a ContractType with a full set of // ContractRevisions in reverse chronological order. Each revision is a change to this @@ -11,7 +11,7 @@ import { NotFoundError } from '../../errors' async function findContractWithHistory( client: PrismaTransactionType, contractID: string -): Promise { +): Promise { try { const contract = await client.contractTable.findFirst({ where: { diff --git a/services/app-api/src/postgres/contractAndRates/submitContract.ts b/services/app-api/src/postgres/contractAndRates/submitContract.ts index 65d165d375..e62153049d 100644 --- a/services/app-api/src/postgres/contractAndRates/submitContract.ts +++ b/services/app-api/src/postgres/contractAndRates/submitContract.ts @@ -1,7 +1,7 @@ import { PrismaClient } from '@prisma/client' import { ContractType } from '../../domain-models/contractAndRates/contractAndRatesZodSchema' import { findContractWithHistory } from './findContractWithHistory' -import { NotFoundError } from '../../errors' +import { NotFoundError } from '../storeError' // Update the given revision // * invalidate relationships of previous revision @@ -11,7 +11,7 @@ async function submitContract( contractID: string, submittedByUserID: string, submitReason: string -): Promise { +): Promise { const groupTime = new Date() try { diff --git a/services/app-api/src/postgres/contractAndRates/unlockContract.ts b/services/app-api/src/postgres/contractAndRates/unlockContract.ts index cc2c2c2552..e579a2adb7 100644 --- a/services/app-api/src/postgres/contractAndRates/unlockContract.ts +++ b/services/app-api/src/postgres/contractAndRates/unlockContract.ts @@ -1,7 +1,7 @@ import { PrismaClient } from '@prisma/client' import { ContractType } from '../../domain-models/contractAndRates/contractAndRatesZodSchema' import { findContractWithHistory } from './findContractWithHistory' -import { NotFoundError } from '../../errors' +import { NotFoundError } from '../storeError' // Unlock the given contract // * copy form data @@ -11,7 +11,7 @@ async function unlockContract( contractID: string, unlockedByUserID: string, unlockReason: string -): Promise { +): Promise { const groupTime = new Date() try { diff --git a/services/app-api/src/postgres/contractAndRates/updateDraftContract.ts b/services/app-api/src/postgres/contractAndRates/updateDraftContract.ts index 82094d45de..79f50dccbf 100644 --- a/services/app-api/src/postgres/contractAndRates/updateDraftContract.ts +++ b/services/app-api/src/postgres/contractAndRates/updateDraftContract.ts @@ -6,7 +6,7 @@ import { } from '@prisma/client' import { ContractType } from '../../domain-models/contractAndRates/contractAndRatesZodSchema' import { findContractWithHistory } from './findContractWithHistory' -import { NotFoundError } from '../../errors' +import { NotFoundError } from '../storeError' type UpdateContractArgsType = { populationCovered: PopulationCoverageType @@ -25,7 +25,7 @@ async function updateDraftContract( contractID: string, formData: UpdateContractArgsType, rateIDs: string[] -): Promise { +): Promise { try { // Given all the Rates associated with this draft, find the most recent submitted // rateRevision to update. diff --git a/services/app-api/src/postgres/index.ts b/services/app-api/src/postgres/index.ts index ec79d0fa33..734bfd32f5 100644 --- a/services/app-api/src/postgres/index.ts +++ b/services/app-api/src/postgres/index.ts @@ -3,5 +3,5 @@ export { InsertHealthPlanPackageArgsType } from './healthPlanPackage' export { InsertUserArgsType } from './user' export { NewPostgresStore, Store } from './postgresStore' export { NewPrismaClient } from './prismaClient' -export { isStoreError, StoreError } from './storeError' +export { isStoreError, StoreError, NotFoundError } from './storeError' export { findStatePrograms } from './state/findStatePrograms' diff --git a/services/app-api/src/postgres/storeError.ts b/services/app-api/src/postgres/storeError.ts index 9ba5e46dab..25d2b61aaa 100644 --- a/services/app-api/src/postgres/storeError.ts +++ b/services/app-api/src/postgres/storeError.ts @@ -90,4 +90,17 @@ const convertPrismaErrorToStoreError = (prismaErr: unknown): StoreError => { } } -export { StoreError, isStoreError, convertPrismaErrorToStoreError } +class NotFoundError extends Error { + constructor(message: string) { + super(message) + + Object.setPrototypeOf(this, NotFoundError.prototype) + } +} + +export { + NotFoundError, + StoreError, + isStoreError, + convertPrismaErrorToStoreError, +} diff --git a/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts b/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts index a13dadfb91..d0511de994 100644 --- a/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts +++ b/services/app-api/src/resolvers/healthPlanPackage/fetchHealthPlanPackage.ts @@ -18,7 +18,7 @@ import { } from '../attributeHelper' import { LDService } from '../../launchDarkly/launchDarkly' import { GraphQLError } from 'graphql/index' -import { NotFoundError } from '../../errors' +import { NotFoundError } from '../../postgres' export function fetchHealthPlanPackageResolver( store: Store, From 5546f33dc0339ea8c146646bba827b2bda5e173e Mon Sep 17 00:00:00 2001 From: Jason Lin Date: Wed, 2 Aug 2023 23:05:27 -0400 Subject: [PATCH 16/16] Fix import on test --- .../src/postgres/contractAndRates/submitContract.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/app-api/src/postgres/contractAndRates/submitContract.test.ts b/services/app-api/src/postgres/contractAndRates/submitContract.test.ts index e2670d3af9..a6ed1034cd 100644 --- a/services/app-api/src/postgres/contractAndRates/submitContract.test.ts +++ b/services/app-api/src/postgres/contractAndRates/submitContract.test.ts @@ -6,7 +6,7 @@ import { insertDraftRate } from './insertRate' import { submitRate } from './submitRate' import { updateDraftRate } from './updateDraftRate' import { must, createInsertContractData } from '../../testHelpers' -import { NotFoundError } from '../../errors' +import { NotFoundError } from '../storeError' describe('submitContract', () => { it('creates a submission from a draft', async () => {