Skip to content

Commit

Permalink
feat: updateField GraphQL mutation (#8291)
Browse files Browse the repository at this point in the history
* feat: notify CC about actions

* refactor: use 'getValidRecordById' as it has the same statuses

* feat: workflow endpoint allowing editing field ids

* fix(tests): mock country config event notification hook

* feat: initial graphql endpoint for update field

* chore: test and fix errors, return boolean

* feat: update the action notification according to events v2

* fix: update test to conform events v2 notification format

* feat: token exchange on notify

* update: test comment

* chore(changelog): add update field

* fix: rename config to options

* refactor: rename token-exchange away from 'handler' as it's not a http handler

* fix: test not mocking the token exchange

* chore: remove unused function

maybe Knip is satisfied after this

* fix: align the error handling with how events service notifies
  • Loading branch information
naftis authored Jan 24, 2025
1 parent 373ef68 commit 4ae0c7a
Show file tree
Hide file tree
Showing 17 changed files with 283 additions and 60 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
- Introduced a new customisable UI component: Banner [#8276](https://github.com/opencrvs/opencrvs-core/issues/8276)
- Auth now allows exchanging user's token for a new record-specific token [#7728](https://github.com/opencrvs/opencrvs-core/issues/7728)
- A new GraphQL mutation `upsertRegistrationIdentifier` is added to allow updating the patient identifiers of a registration record such as NID [#8034](https://github.com/opencrvs/opencrvs-core/pull/8034)
- A new GraphQL mutation `updateField` is added to allow updating any field in a record [#8291](https://github.com/opencrvs/opencrvs-core/pull/8291)
- Updated GraphQL mutation `confirmRegistration` to allow adding a `comment` for record audit [#8197](https://github.com/opencrvs/opencrvs-core/pull/8197)

### Improvements
Expand Down
12 changes: 11 additions & 1 deletion packages/gateway/src/features/registration/root-resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ import {
markNotADuplicate,
rejectRegistration,
confirmRegistration,
upsertRegistrationIdentifier
upsertRegistrationIdentifier,
updateField
} from '@gateway/workflow/index'
import { getRecordById } from '@gateway/records'
import { SCOPES } from '@opencrvs/commons/authentication'
Expand Down Expand Up @@ -674,6 +675,15 @@ export const resolvers: GQLResolver = {
} catch (error) {
throw new Error(`Failed to confirm registration: ${error.message}`)
}
},
async updateField(_, { id, details }, { headers: authHeader }) {
if (!hasRecordAccess(authHeader, id)) {
throw new Error('User does not have access to the record')
}

await updateField(id, authHeader, details)

return true
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions packages/gateway/src/features/registration/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,12 @@ input RejectRegistrationInput {
comment: String
}

input UpdateFieldInput {
fieldId: String!
valueString: String
valueBoolean: Boolean
}

type Mutation {
# Generic correction handlers for all event types
# Applying a correction request is made on a event level as payload is dependant on event type
Expand Down Expand Up @@ -662,4 +668,5 @@ type Mutation {
identifierType: String!
identifierValue: String!
): ID!
updateField(id: ID!, details: UpdateFieldInput!): Boolean!
}
21 changes: 21 additions & 0 deletions packages/gateway/src/graphql/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export interface GQLMutation {
confirmRegistration: string
rejectRegistration: string
upsertRegistrationIdentifier: string
updateField: boolean
createOrUpdateUser: GQLUser
activateUser?: string
changePassword?: string
Expand Down Expand Up @@ -574,6 +575,12 @@ export interface GQLRejectRegistrationInput {
comment?: string
}

export interface GQLUpdateFieldInput {
fieldId: string
valueString?: string
valueBoolean?: boolean
}

export interface GQLUserInput {
id?: string
name: Array<GQLHumanNameInput>
Expand Down Expand Up @@ -2376,6 +2383,7 @@ export interface GQLMutationTypeResolver<TParent = any> {
confirmRegistration?: MutationToConfirmRegistrationResolver<TParent>
rejectRegistration?: MutationToRejectRegistrationResolver<TParent>
upsertRegistrationIdentifier?: MutationToUpsertRegistrationIdentifierResolver<TParent>
updateField?: MutationToUpdateFieldResolver<TParent>
createOrUpdateUser?: MutationToCreateOrUpdateUserResolver<TParent>
activateUser?: MutationToActivateUserResolver<TParent>
changePassword?: MutationToChangePasswordResolver<TParent>
Expand Down Expand Up @@ -2956,6 +2964,19 @@ export interface MutationToUpsertRegistrationIdentifierResolver<
): TResult
}

export interface MutationToUpdateFieldArgs {
id: string
details: GQLUpdateFieldInput
}
export interface MutationToUpdateFieldResolver<TParent = any, TResult = any> {
(
parent: TParent,
args: MutationToUpdateFieldArgs,
context: Context,
info: GraphQLResolveInfo
): TResult
}

export interface MutationToCreateOrUpdateUserArgs {
user: GQLUserInput
}
Expand Down
7 changes: 7 additions & 0 deletions packages/gateway/src/graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ type Mutation {
identifierType: String!
identifierValue: String!
): ID!
updateField(id: ID!, details: UpdateFieldInput!): Boolean!
createOrUpdateUser(user: UserInput!): User!
activateUser(
userId: String!
Expand Down Expand Up @@ -691,6 +692,12 @@ input RejectRegistrationInput {
comment: String
}

input UpdateFieldInput {
fieldId: String!
valueString: String
valueBoolean: Boolean
}

input UserInput {
id: ID
name: [HumanNameInput!]!
Expand Down
21 changes: 21 additions & 0 deletions packages/gateway/src/workflow/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,27 @@ export async function upsertRegistrationIdentifier(
return taskEntry
}

type UpdateFieldInput = {
fieldId: string
valueString?: string
valueBoolean?: boolean
}

export async function updateField(
id: string,
authHeader: IAuthHeader,
details: UpdateFieldInput
) {
const res = await createRequest<ValidRecord>(
'POST',
`/records/${id}/update-field`,
authHeader,
details
)

return res
}

export async function archiveRegistration(
id: string,
authHeader: IAuthHeader,
Expand Down
1 change: 1 addition & 0 deletions packages/workflow/src/__mocks__/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const USER_MANAGEMENT_URL = 'http://localhost:3030/'
export const SEARCH_URL = 'http://localhost:9090/'
export const METRICS_URL = 'http://localhost:1050/'
export const DOCUMENTS_URL = 'http://localhost:9050'
export const AUTH_URL = 'http://localhost:4040/'
export const NOTIFICATION_SERVICE_URL = 'http://localhost:2020/'
export const APPLICATION_CONFIG_URL = 'http://localhost:2021/'
export const COUNTRY_CONFIG_URL = 'http://localhost:3040'
Expand Down
12 changes: 11 additions & 1 deletion packages/workflow/src/config/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { eventNotificationHandler } from '@workflow/records/handler/eventNotific
import * as Hapi from '@hapi/hapi'
import { SCOPES } from '@opencrvs/commons/authentication'
import { upsertRegistrationHandler } from '@workflow/records/handler/upsert-identifiers'
import { updateField } from '@workflow/records/handler/update-field'

export const getRoutes = () => {
const routes: Hapi.ServerRoute[] = [
Expand Down Expand Up @@ -175,7 +176,16 @@ export const getRoutes = () => {
approveCorrectionRoute,
rejectCorrectionRoute,
requestCorrectionRoute,
makeCorrectionRoute
makeCorrectionRoute,
{
method: 'POST',
path: '/records/{id}/update-field',
handler: updateField,
options: {
tags: ['api'],
description: 'Update a single field in a registration'
}
}
]

return routes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

import { UUID } from '@opencrvs/commons'
import {
Bundle,
Composition,
Patient,
Practitioner,
Expand All @@ -25,36 +24,10 @@ import {
SupportedPatientIdentifierCode,
ValidRecord
} from '@opencrvs/commons/types'
import { COUNTRY_CONFIG_URL } from '@workflow/constants'
import { OPENCRVS_SPECIFICATION_URL } from '@workflow/features/registration/fhir/constants'
import { getSectionEntryBySectionCode } from '@workflow/features/registration/fhir/fhir-template'
import { getPractitionerRef } from '@workflow/features/user/utils'
import { isEqual } from 'lodash'
import fetch from 'node-fetch'

export async function invokeRegistrationValidation(
bundle: Saved<Bundle>,
headers: Record<string, string>
): Promise<Bundle> {
const res = await fetch(
new URL('event-registration', COUNTRY_CONFIG_URL).toString(),
{
method: 'POST',
body: JSON.stringify(bundle),
headers: {
'Content-Type': 'application/json',
...headers
}
}
)
if (!res.ok) {
const errorText = await res.text()
throw new Error(
`Error calling country configuration event-registration [${res.statusText} ${res.status}]: ${errorText}`
)
}
return bundle
}

export function setupLastRegOffice<T extends Task>(
taskResource: T,
Expand Down
13 changes: 0 additions & 13 deletions packages/workflow/src/features/registration/fhir/fhir-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,19 +312,6 @@ export function generateEmptyBundle(): Bundle {
}
}

export async function fetchExistingRegStatusCode(taskId: string | undefined) {
const existingTaskResource: Task = await getFromFhir(`/Task/${taskId}`)
const existingRegStatusCode =
existingTaskResource &&
existingTaskResource.businessStatus &&
existingTaskResource.businessStatus.coding &&
existingTaskResource.businessStatus.coding.find((code) => {
return code.system === `${OPENCRVS_SPECIFICATION_URL}reg-status`
})

return existingRegStatusCode
}

function mergeFhirIdentifiers(
currentIdentifiers: fhir3.Identifier[],
newIdentifiers: fhir3.Identifier[]
Expand Down
31 changes: 31 additions & 0 deletions packages/workflow/src/records/handler/create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,29 @@ describe('Create record endpoint', () => {
}
)

// Notification endpoint mockcall
mswServer.use(
rest.get('http://localhost:3040/record-notification', (_, res, ctx) => {
return res(ctx.json({}))
})
)

// Token exchange mock call
mswServer.use(
// The actual more verbose query below, but for simplicity we can keep simpler one unless this causes issues:

// ?grant_type=urn:opencrvs:oauth:grant-type:token-exchange&subject_token=${token}&subject_token_type=urn:ietf:params:oauth:token-type:access_token
// &requested_token_type=urn:opencrvs:oauth:token-type:single_record_token&record_id=${recordId}

rest.post(`http://localhost:4040/token`, (_, res, ctx) => {
return res(
ctx.json({
access_token: 'some-token'
})
)
})
)

// used for checking already created composition with
// the same draftId
mswServer.use(
Expand Down Expand Up @@ -157,6 +180,14 @@ describe('Create record endpoint', () => {
})
)

// mock country config event action hook returning a basic 200
mswServer.use(
rest.post(
'http://localhost:3040/events/BIRTH/actions/sent-notification-for-review',
(_, res, ctx) => res(ctx.status(200))
)
)

const res = await server.server.inject({
method: 'POST',
url: '/create-record',
Expand Down
22 changes: 22 additions & 0 deletions packages/workflow/src/records/handler/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ import {
toWaitingForExternalValidationState
} from '@workflow/records/state-transitions'
import { logger, UUID } from '@opencrvs/commons'
import { notifyForAction } from '@workflow/utils/country-config-api'
import { getRecordSpecificToken } from '@workflow/records/token-exchange'

const requestSchema = z.object({
event: z.custom<EVENT_TYPE>(),
Expand Down Expand Up @@ -348,6 +350,8 @@ export default async function createRecordHandler(
/*
* We need to initiate registration for a
* record in waiting validation state
*
* `initiateRegistration` notifies country configuration about the event which then either confirms or rejects the record.
*/
if (isWaitingExternalValidation(record)) {
const rejectedOrWaitingValidationRecord = await initiateRegistration(
Expand All @@ -360,6 +364,24 @@ export default async function createRecordHandler(
await indexBundle(rejectedOrWaitingValidationRecord, token)
await auditEvent('sent-for-updates', record, token)
}
} else {
/*
* Notify country configuration about the event so that countries can hook into actions like "sent-for-approval"
*/
const recordSpecificToken = await getRecordSpecificToken(
token,
request.headers,
getComposition(record).id
)
await notifyForAction({
event,
action: eventAction,
record,
headers: {
...request.headers,
authorization: `Bearer ${recordSpecificToken.access_token}`
}
})
}

return {
Expand Down
Loading

0 comments on commit 4ae0c7a

Please sign in to comment.