-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(GIFT-3546): add endpoint to fetch customer data #1127
base: feat/FN-3543/integrate-with-ods
Are you sure you want to change the base?
feat(GIFT-3546): add endpoint to fetch customer data #1127
Conversation
… and update .env.sample
@@ -10,7 +10,7 @@ run-name: Executing test QA on ${{ github.repository }} 🚀 | |||
|
|||
on: | |||
pull_request: | |||
branches: [main] | |||
branches: [main, feat/FN-3543/integrate-with-ods] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
temporarily leaving this in while the feature branch is open to run tests, will remove before feature branch is merged to main
pattern: regexToString(UKEFID.PARTY_ID.REGEX), | ||
}) | ||
@Matches(UKEFID.PARTY_ID.REGEX) | ||
public customerUrn: string; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The endpoint and ODS refer to the entity as customer so I've used customer URN instead of party URN here but I'm happy to change back to partyUrn if people think that is more logical/consistent
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Opted to just call it urn while in the namespace of customer
@ApiProperty({ | ||
description: 'The unique UKEF id of the customer', | ||
}) | ||
readonly customerUrn: string; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto on customerUrn comment on the other dto
import { OdsController } from './ods.controller'; | ||
import { OdsService } from './ods.service'; | ||
|
||
describe('OdsController', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted a test to check the validation of the dto (e.g. valid when a urn matching the regex is passed through) but that would require setting up a validationPipe for this test which no other test in the codebase has done. For now I'm going to leave out to stay consistent with the other test suites but one to look at across the board
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At the moment DTO validations are tested as part of api/e2e
tests (root/tests
). Most likely these tests are not part of github pipelines yet.
Would be nice to create api/e2e
test for osd
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep will add this in a new PR straight after this one (don't have a tonne of time today/tomorrow but keen to get my feature branch tested in dev so have split up the work)
@Injectable() | ||
export class OdsService { | ||
constructor( | ||
@InjectDataSource(DATABASE.ODS) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This black box magic of adding the name of the data source to pick the right one is amazing, I don't fully understand it but I like it
customerName: storedProcedureJson.results[0]?.customer_name, | ||
}; | ||
} catch (err) { | ||
if (err instanceof NotFoundException) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the catching and rethrowing feels a little clunky here but is the best way to get better logging for debugging which I think is worth it. Any recommendations on refactoring this are welcome
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Its not ideal but to be honest, I can see reminisense of this pattern elsewhere in the codebase. Ideally an exceptional class will be clean but might creep the scope of this PR.
const queryRunner = this.odsDataSource.createQueryRunner(); | ||
try { | ||
// Use the query runner to call a stored procedure | ||
const result = await queryRunner.query( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortuantely typeorm doesn't support any calling of stored procedures outside of just a SQL query so I've written a parameterised SQL query to stop SQL injection but has to be hard coded like this
…cumentation and small fixes
Quality Gate passedIssues Measures |
|
||
expect(mockDataSource.createQueryRunner).toHaveBeenCalled(); | ||
expect(mockQueryRunner.query).toHaveBeenCalledWith( | ||
expect.stringMatching( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't want to have to match indentation in the string matching so there is a regex match instead. Thoughts @toddym42 ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure I can think of anything better if we want to match the whole string, but I'm wondering if there's much value asserting on the whole thing, as it's just a hard-coded value that we're repeating. Could just expect any string, or something more minimal like expect.stringContaining('EXEC t_apim.sp_ODS_query')
?
@ApiResponse({ | ||
status: 200, | ||
description: 'Customers matching search parameters', | ||
type: GetOdsCustomerResponse, | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not requesting to change, but just an fyi that there is a more specific @ApiOkResponse
that we could use here, like with the error responses.
|
||
expect(mockDataSource.createQueryRunner).toHaveBeenCalled(); | ||
expect(mockQueryRunner.query).toHaveBeenCalledWith( | ||
expect.stringMatching( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure I can think of anything better if we want to match the whole string, but I'm wondering if there's much value asserting on the whole thing, as it's just a hard-coded value that we're repeating. Could just expect any string, or something more minimal like expect.stringContaining('EXEC t_apim.sp_ODS_query')
?
}); | ||
|
||
describe('findCustomer', () => { | ||
it('findCustomer should return a customer the customer urn and name when findCustomer is called', async () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it('findCustomer should return a customer the customer urn and name when findCustomer is called', async () => { | |
it('should return a customer, the customer urn, and name when findCustomer is called', async () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missed this one.
|
||
const storedProcedureJson: OdsStoredProcedureOuputBody = JSON.parse(storedProcedureResult[0]?.output_body); | ||
|
||
if (storedProcedureJson === undefined || storedProcedureJson?.status != 'SUCCESS') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (storedProcedureJson === undefined || storedProcedureJson?.status != 'SUCCESS') { | |
if (storedProcedureJson === undefined || storedProcedureJson?.status !== 'SUCCESS') { |
(sorry I missed this first time round 😅)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can it not just be storedProcedureJson?.status !== 'SUCCESS'
, is there a reason for an explicity undefined comparison?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was there for code readability showing that it can be undefined rather than any required programmatical check, I'm happy to remove
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work, few minor and medium comments to address please.
@ApiProperty({ | ||
required: true, | ||
example: CUSTOMERS.EXAMPLES.PARTYURN, | ||
description: 'The unique UKEF id of the customer to search for.', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
description: 'The unique UKEF id of the customer to search for.', | |
description: 'The unique UKEF ID of the customer to search for.', |
I know this exists else where, will raise a seprate PR to rectify this minor typo.
pattern: regexToString(UKEFID.PARTY_ID.REGEX), | ||
}) | ||
@Matches(UKEFID.PARTY_ID.REGEX) | ||
public urn: string; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be a number?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because a lot of urns start with 00 we don't want to cast this to number, has to stay a string
|
||
export class GetOdsCustomerResponse { | ||
@ApiProperty({ | ||
description: 'The unique UKEF id of the customer', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
description: 'The unique UKEF id of the customer', | |
description: 'The unique UKEF ID of the customer', |
description: 'The unique UKEF id of the customer', | ||
example: '00312345', | ||
}) | ||
readonly urn: string; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Number?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto other comment: Because a lot of urns start with 00 we don't want to cast this to number, has to stay a string
}; | ||
|
||
export const ODS_ENTITIES = { | ||
CUSTOMER: 'customer', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a constant for this? If not then don't worry will have to be a separate PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a constant I've defined to have all the different entities that can be queried in ODS
customerName: storedProcedureJson.results[0]?.customer_name, | ||
}; | ||
} catch (err) { | ||
if (err instanceof NotFoundException) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Its not ideal but to be honest, I can see reminisense of this pattern elsewhere in the codebase. Ideally an exceptional class will be clean but might creep the scope of this PR.
*/ | ||
async callOdsStoredProcedure(storedProcedureInput: OdsStoredProcedureInput): Promise<OdsStoredProcedureOutput> { | ||
const queryRunner = this.odsDataSource.createQueryRunner(); | ||
try { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you not need await queryRunner.connect();
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good catch!
[JSON.stringify(storedProcedureInput)], | ||
); | ||
|
||
return result; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return result; | |
// Return the data or falsy | |
return result[0]?.output_body || null; |
); | ||
|
||
return result; | ||
} finally { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missed catch block.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didnt' have a catch block here as it'll be caught in the function above - I can add one which will just be:
catch (error) {
throw error
}
It won't add or do anything but if it's a pattern we want to stick to will add it
EXEC t_apim.sp_ODS_query @input_body=@0, @output_body=@output_body OUTPUT; | ||
SELECT @output_body as output_body | ||
`, | ||
[JSON.stringify(storedProcedureInput)], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Is
storedProcedureInput
validated before passing it to the query? - Does SP has enought validation to ensure no malcious code wil be executed.
- Does SP has correct ACL i.e. role management?
- Error handling should also be sanitized to ensure error messages do not expose anything sensitive back to the client.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- we create storedProcedureInput from code and from a validated input to the endpoint (urn).
- we use parameterisation to stop SQL injection
- The user (service principal) calling this stored procedure has a very tight ACL, it can only run this stored procedure and query one logs table, it can't do anything else
- None of our error handling uses the output of the stored procedure, we only log that to Azure server logs
Introduction ✏️
Create an endpoint and new ODS service/controller/module to query ODS for the customer.
Added types and functionality for new endpoints for other ODS entities to be fetched in the future.
All entities from ODS will be fetched in a similar way using the same stored procedure hence lumping it all together in a single ODS service, we could look at splitting up controllers and services in the future if we feel they are getting too bloated.
No full payload validation has been done of the response from the ODS stored procedures - only validation of the fields that is required for the endpoint.
An API test will be added separately to this PR in a following PR