Skip to content

Commit

Permalink
safe parse with zod
Browse files Browse the repository at this point in the history
  • Loading branch information
louptheron committed Feb 4, 2025
1 parent 571df76 commit bbf7384
Show file tree
Hide file tree
Showing 10 changed files with 94 additions and 22 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ jobs:
uses: actions/checkout@v4

- name: Download image
uses: ishworkh/container-image-artifact-upload@v2.0.0
uses: ishworkh/container-image-artifact-download@v2.0.0
with:
image: monitorfish-app:${{ env.VERSION }}

Expand Down
10 changes: 5 additions & 5 deletions frontend/cypress/e2e/main_window/vessel_sidebar/logbook.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,9 @@ context('Vessel sidebar logbook tab', () => {
.its('response.url')
.should(
'have.string',
encodeURIComponent('/bff/v1/vessels/positions?afterDateTime=2019-02-16T21:05:00.000Z' +
'&beforeDateTime=2019-10-15T13:01:00.000Z&externalReferenceNumber=DONTSINK&internalReferenceNumber=FAK000999999' +
'&IRCS=CALLME&trackDepth=CUSTOM&vesselIdentifier=INTERNAL_REFERENCE_NUMBER')
`/bff/v1/vessels/positions?afterDateTime=${encodeURIComponent('2019-02-16T21:05:00.000Z')}` +
`&beforeDateTime=${encodeURIComponent('2019-10-15T13:01:00.000Z')}&externalReferenceNumber=DONTSINK&internalReferenceNumber=FAK000999999` +
'&IRCS=CALLME&trackDepth=CUSTOM&vesselIdentifier=INTERNAL_REFERENCE_NUMBER'
)

cy.get('*[data-cy^="fishing-activity-name"]').should('exist').should('have.length', 4)
Expand All @@ -171,9 +171,9 @@ context('Vessel sidebar logbook tab', () => {
.its('response.url')
.should(
'have.string',
encodeURIComponent('/bff/v1/vessels/positions?&afterDateTime=&beforeDateTime=' +
'/bff/v1/vessels/positions?&afterDateTime=&beforeDateTime=' +
'&externalReferenceNumber=DONTSINK&internalReferenceNumber=FAK000999999' +
'&IRCS=CALLME&trackDepth=TWELVE_HOURS&vesselIdentifier=INTERNAL_REFERENCE_NUMBER')
'&IRCS=CALLME&trackDepth=TWELVE_HOURS&vesselIdentifier=INTERNAL_REFERENCE_NUMBER'
)
cy.get('*[data-cy^="fishing-activity-name"]').should('not.exist')
// Go back to the default track depth
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ context('Offline management', () => {
{
method: 'GET',
path:
'bff/v1/vessels/find?afterDateTime=&beforeDateTime=&externalReferenceNumber=DONTSINK' +
'/bff/v1/vessels/find?afterDateTime=&beforeDateTime=&externalReferenceNumber=DONTSINK' +
'&internalReferenceNumber=FAK000999999&IRCS=CALLME&trackDepth=TWELVE_HOURS' +
'&vesselId=1&vesselIdentifier=INTERNAL_REFERENCE_NUMBER',
times: 2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ context('Side Window > Beacon Malfunction Board', () => {
'GET',
new RegExp(
`bff\\/v1\\/vessels\\/find\\?` +
`&afterDateTime=${oneWeeksBeforeDate.format('YYYY-MM-DD')}T\\d{2}%3A\\d{2}%3A\\d{2}\\.\\d{3}Z` +
`afterDateTime=${oneWeeksBeforeDate.format('YYYY-MM-DD')}T\\d{2}%3A\\d{2}%3A\\d{2}\\.\\d{3}Z` +
`&beforeDateTime=${oneWeeksBeforePlusOneDayDate.format('YYYY-MM-DD')}T\\d{2}%3A\\d{2}%3A\\d{2}\\.\\d{3}Z` +
`&externalReferenceNumber=DONTSINK&internalReferenceNumber=FAK000999999` +
`&IRCS=CALLME&trackDepth=CUSTOM&vesselId=1&vesselIdentifier=INTERNAL_REFERENCE_NUMBER`
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/api/alert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const alertApi = monitorfishApi.injectEndpoints({
transformErrorResponse: response => new FrontendApiError(DELETE_SILENCED_ALERT_ERROR_MESSAGE, response)
}),
getOperationalAlerts: builder.query<LEGACY_PendingAlert[], void>({
query: () => '/bff/v1/operational_alerts',
query: () => '/operational_alerts',
transformErrorResponse: response => new FrontendApiError(ALERTS_ERROR_MESSAGE, response),
transformResponse: (response: PendingAlert[]) => response.map(normalizePendingAlert)
}),
Expand Down
25 changes: 14 additions & 11 deletions frontend/src/features/FleetSegment/apis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { FleetSegmentSchema } from '@features/FleetSegment/types'
import { MissionAction } from '@features/Mission/missionAction.types'
import { FrontendApiError } from '@libs/FrontendApiError'
import { customDayjs } from '@mtes-mct/monitor-ui'
import { parseResponseOrReturn } from '@utils/parseResponseOrReturn'

import type { FleetSegment } from '@features/FleetSegment/types'

Expand Down Expand Up @@ -43,9 +44,9 @@ export const fleetSegmentApi = monitorfishApi.injectEndpoints({
url: `/fleet_segments/compute`
}),
transformResponse: (baseQueryReturnValue: FleetSegment[]) =>
baseQueryReturnValue
.map(segment => FleetSegmentSchema.parse(segment))
.sort((a, b) => a.segment.localeCompare(b.segment))
parseResponseOrReturn<FleetSegment>(baseQueryReturnValue, FleetSegmentSchema, true).sort((a, b) =>
a.segment.localeCompare(b.segment)
)
}),
createFleetSegment: builder.mutation<FleetSegment, FleetSegment>({
query: segmentFields => ({
Expand All @@ -54,7 +55,8 @@ export const fleetSegmentApi = monitorfishApi.injectEndpoints({
url: '/admin/fleet_segments'
}),
transformErrorResponse: response => new FrontendApiError(CREATE_FLEET_SEGMENT_ERROR_MESSAGE, response),
transformResponse: (baseQueryReturnValue: FleetSegment) => FleetSegmentSchema.parse(baseQueryReturnValue)
transformResponse: (baseQueryReturnValue: FleetSegment) =>
parseResponseOrReturn<FleetSegment>(baseQueryReturnValue, FleetSegmentSchema, false)
}),
deleteFleetSegment: builder.mutation<FleetSegment[], { segment: string; year: number }>({
query: ({ segment, year }) => ({
Expand All @@ -63,9 +65,9 @@ export const fleetSegmentApi = monitorfishApi.injectEndpoints({
}),
transformErrorResponse: response => new FrontendApiError(DELETE_FLEET_SEGMENT_ERROR_MESSAGE, response),
transformResponse: (baseQueryReturnValue: FleetSegment[]) =>
baseQueryReturnValue
.map(segment => FleetSegmentSchema.parse(segment))
.sort((a, b) => a.segment.localeCompare(b.segment))
parseResponseOrReturn<FleetSegment>(baseQueryReturnValue, FleetSegmentSchema, true).sort((a, b) =>
a.segment.localeCompare(b.segment)
)
}),
getFleetSegments: builder.query<FleetSegment[], number | void>({
providesTags: () => [{ type: 'FleetSegments' }],
Expand All @@ -75,9 +77,9 @@ export const fleetSegmentApi = monitorfishApi.injectEndpoints({
return `fleet_segments/${controlledYear}`
},
transformResponse: (baseQueryReturnValue: FleetSegment[]) =>
baseQueryReturnValue
.map(segment => FleetSegmentSchema.parse(segment))
.sort((a, b) => a.segment.localeCompare(b.segment))
parseResponseOrReturn<FleetSegment>(baseQueryReturnValue, FleetSegmentSchema, true).sort((a, b) =>
a.segment.localeCompare(b.segment)
)
}),
getFleetSegmentYearEntries: builder.query<number[], void>({
query: () => '/admin/fleet_segments/years',
Expand All @@ -94,7 +96,8 @@ export const fleetSegmentApi = monitorfishApi.injectEndpoints({
}
},
transformErrorResponse: response => new FrontendApiError(UPDATE_FLEET_SEGMENT_ERROR_MESSAGE, response),
transformResponse: (baseQueryReturnValue: FleetSegment) => FleetSegmentSchema.parse(baseQueryReturnValue)
transformResponse: (baseQueryReturnValue: FleetSegment) =>
parseResponseOrReturn<FleetSegment>(baseQueryReturnValue, FleetSegmentSchema, false)
})
})
})
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/features/Vessel/vesselApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { VesselLastPositionSchema } from '@features/Vessel/schemas/VesselLastPos
import { DisplayedErrorKey } from '@libs/DisplayedError/constants'
import { FrontendApiError } from '@libs/FrontendApiError'
import { getUrlOrPathWithQueryParams } from '@utils/getUrlOrPathWithQueryParams'
import { parseResponseOrReturn } from '@utils/parseResponseOrReturn'
import { displayedErrorActions } from 'domain/shared_slices/DisplayedError'
import { displayOrLogError } from 'domain/use_cases/error/displayOrLogError'

Expand Down Expand Up @@ -140,7 +141,7 @@ export const vesselApi = monitorfishApi.injectEndpoints({
getVesselsLastPositions: builder.query<Vessel.VesselLastPosition[], void>({
query: () => `/vessels`,
transformResponse: (baseQueryReturnValue: Vessel.VesselLastPosition[]) =>
baseQueryReturnValue.map(LastPosition => VesselLastPositionSchema.parse(LastPosition))
parseResponseOrReturn<Vessel.VesselLastPosition>(baseQueryReturnValue, VesselLastPositionSchema, true)
}),

searchVessels: builder.query<Vessel.VesselIdentity[], Vessel.ApiSearchFilter>({
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/features/Vessel/vesselNavApi.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { monitorfishLightApi } from '@api/api'
import { VesselLastPositionLightSchema } from '@features/Vessel/schemas/VesselLastPositionLightSchema'
import { Vessel } from '@features/Vessel/Vessel.types'
import { parseResponseOrReturn } from '@utils/parseResponseOrReturn'

export const vesselNavApi = monitorfishLightApi.injectEndpoints({
endpoints: builder => ({
getVesselsLastPositions: builder.query<Vessel.VesselLightLastPosition[], void>({
query: () => `/v1/vessels`,
transformResponse: (baseQueryReturnValue: Vessel.VesselLightLastPosition[]) =>
baseQueryReturnValue.map(LastPosition => VesselLastPositionLightSchema.parse(LastPosition))
parseResponseOrReturn<Vessel.VesselLightLastPosition>(baseQueryReturnValue, VesselLastPositionLightSchema, true)
})
})
})
Expand Down
44 changes: 44 additions & 0 deletions frontend/src/utils/__tests__/parseResponseOrReturn.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { FleetSegmentSchema } from '@features/FleetSegment/types'
import { VesselLastPositionSchema } from '@features/Vessel/schemas/VesselLastPositionSchema'
import { expect } from '@jest/globals'
import { parseResponseOrReturn } from '@utils/parseResponseOrReturn'

describe('utils/parseResponseOrReturn()', () => {
it('should return the original response and print an error', () => {
const object = { dummy: true }

const result = parseResponseOrReturn(object, VesselLastPositionSchema, false)

expect(result).toStrictEqual(object)
})

it('should return the original response and print an error with an array', () => {
const object = { dummy: true }

const result = parseResponseOrReturn([object], VesselLastPositionSchema, true)

expect(result).toStrictEqual([object])
})

it('should validate the type successfully', () => {
const segment = {
faoAreas: [],
gears: [],
impactRiskFactor: 2.0,
mainScipSpeciesType: undefined,
maxMesh: undefined,
minMesh: undefined,
minShareOfTargetSpecies: undefined,
priority: 0,
segment: '',
segmentName: '',
targetSpecies: [],
vesselTypes: [],
year: 2021
}

const result = parseResponseOrReturn(segment, FleetSegmentSchema, false)

expect(result).toStrictEqual(segment)
})
})
23 changes: 23 additions & 0 deletions frontend/src/utils/parseResponseOrReturn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { FrontendError } from '@libs/FrontendError'
import { ZodSchema } from 'zod'

export function parseResponseOrReturn<T>(body: unknown, schema: ZodSchema<any>, isArray: false): T
export function parseResponseOrReturn<T>(body: unknown, schema: ZodSchema<any>, isArray: true): T[]
export function parseResponseOrReturn<T>(body: unknown, schema: ZodSchema<any>, isArray: boolean): T | T[] {
try {
if (!isArray) {
return schema.parse(body)
}

if (!Array.isArray(body)) {
throw new Error('Expected an array for parsing.')
}

return body.map(bodyElement => schema.parse(bodyElement))
} catch (e) {
// eslint-disable-next-line no-new
new FrontendError('Failing validating type', e)

return body as T | T[]
}
}

0 comments on commit bbf7384

Please sign in to comment.