Skip to content
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

Y24-149 exhaust samples moved to destroyed location #2121

Open
wants to merge 27 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
0ab2553
Update environment configuration files to include DESTROYED_LOCATION_…
seenanair Dec 16, 2024
ecae3df
update(client.js): Add exhaustSamplesIfDestroyed function to handle d…
seenanair Dec 16, 2024
5d9c54c
refactor(pacbioLibraries): Simplify library fetching and transformati…
seenanair Dec 16, 2024
2f76ac3
update(pacbioLibraries): Add library validation and fetching functions
seenanair Dec 16, 2024
6aaa5a2
update(LabwhereReception): Integrate exhaustSamplesIfDestroyed for ex…
seenanair Dec 16, 2024
0092a8a
Add unit tests
seenanair Dec 16, 2024
47737a0
rename: Update function name to exhaustLibraryVolumeIfDestroyed for c…
seenanair Dec 16, 2024
2f7e594
Update pacbioLibraries.js
seenanair Dec 16, 2024
1cd5b7c
Update LabwhereReception.vue
seenanair Dec 16, 2024
6ffb245
tests
seenanair Dec 16, 2024
f77f9e8
refactor: remove createLibrariesArray
seenanair Dec 16, 2024
c96544e
Update pacbioLibraries.spec.js
seenanair Dec 16, 2024
9546b61
Update visit_labwhere_reception_page.cy.js
seenanair Dec 16, 2024
54d03a4
added documentation and prettier updates
seenanair Dec 16, 2024
12ea25e
Merge branch 'develop' into y24-149-exhaust-samples-moved-to-destroye…
seenanair Dec 16, 2024
c207430
update: removed v2 namespace
seenanair Dec 16, 2024
f9bcd24
feat: add PacbioLibrary service for fetching and updating library res…
seenanair Jan 3, 2025
c99b7cb
refactor: replace fetchLibraries with getPacbioLibraryResources in la…
seenanair Jan 3, 2025
8a46b56
refactor: update library resource handling in pacbioLibraries store
seenanair Jan 3, 2025
995d5cc
refactor: simplify library update logic and remove unused fetchLibrar…
seenanair Jan 3, 2025
c550d0c
test updates
seenanair Jan 3, 2025
df19bef
feat: add exhausted badge to volume cell in PacbioLibraryIndex component
seenanair Jan 6, 2025
9bc7b99
test updates
seenanair Jan 6, 2025
217a3eb
Update .env.development
seenanair Jan 7, 2025
d55f4bd
Update .env.development
seenanair Jan 7, 2025
a7b7160
refactor: dependency injection to avoid global fetch mocking
seenanair Jan 7, 2025
b468bfc
Merge branch 'develop' into y24-149-exhaust-samples-moved-to-destroye…
seenanair Jan 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ VITE_TRACTION_BASE_URL=http://traction
VITE_TRACTION_GRAPHQL_URL=http://traction/graphql
VITE_PRINTMYBARCODE_BASE_URL=http://printmybarcode
VITE_SAMPLEEXTRACTION_BASE_URL=http://sampleextraction
VITE_LABWHERE_BASE_URL=http://labwhere
VITE_LABWHERE_BASE_URL=http://localhost:3003
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is better to do this in .env.development.local

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an e2e test to check for volume exhaustion when the entered location barcode matches the destroyed location barcode, as specified in the corresponding .env file. This test will fail during CI if the correct configuration is not set. Is there are any better approaches to handle this scenario?

Copy link
Collaborator

@stevieing stevieing Jan 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok. That is a problem. How does it work for all of our other api calls. We don't set traction url to a real one? It sounds like the intercept is not being handled correctly maybe?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, we are simply comparing the entered barcode with the value of the environment variable read within the method. I think this is different from other cases where we can intercept commands to mock network requests and responses, eliminating the need for the real ones. I tried various approaches, such as using Cypress config to define a value, but it didn't work

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand now. It is the barcode you are checking which is why it needs to be in .env

VITE_SEQUENCESCAPE_API_KEY=development
VITE_LOG=true
VITE_ENVIRONMENT=development
VITE_ENVIRONMENT=development
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are youn planning to add the destroyed location to the deployment project or just the .env files?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is already added in deployment project https://github.com/sanger/deployment/pull/533

VITE_DESTROYED_LOCATION_BARCODE=lw-destroyed-217
3 changes: 2 additions & 1 deletion .env.production
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ VITE_SAMPLEEXTRACTION_BASE_URL=REPLACE_VITE_SAMPLEEXTRACTION_BASE_URL
VITE_SEQUENCESCAPE_BASE_URL=REPLACE_VITE_SEQUENCESCAPE_BASE_URL
VITE_LABWHERE_BASE_URL=REPLACE_VITE_LABWHERE_BASE_URL
VITE_SEQUENCESCAPE_API_KEY=REPLACE_VITE_SEQUENCESCAPE_API_KEY
VITE_ENVIRONMENT=REPLACE_VITE_ENVIRONMENT
VITE_ENVIRONMENT=REPLACE_VITE_ENVIRONMENT
VITE_DESTROYED_LOCATION_BARCODE=REPLACE_VITE_LABWHERE_DESTROY_LOCATION_BARCODE
1 change: 1 addition & 0 deletions .env.uat
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ VITE_SEQUENCESCAPE_BASE_URL=REPLACE_VITE_SEQUENCESCAPE_BASE_URL
VITE_LABWHERE_BASE_URL=REPLACE_VITE_LABWHERE_BASE_URL
VITE_SEQUENCESCAPE_API_KEY=REPLACE_VITE_SEQUENCESCAPE_API_KEY
VITE_ENVIRONMENT=REPLACE_VITE_ENVIRONMENT
VITE_DESTROYED_LOCATION_BARCODE=REPLACE_VITE_LABWHERE_DESTROY_LOCATION_BARCODE
68 changes: 63 additions & 5 deletions src/services/labwhere/client.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { extractLocationsForLabwares } from './helpers.js'
import { FetchWrapper } from '@/api/FetchWrapper.js'
import {
exhaustLibrayVolume,
formatAndTransformLibraries,
} from '@/stores/utilities/pacbioLibraries.js'
import { getPacbioLibraryResources } from '@/services/traction/PacbioLibrary.js'

const labwhereFetch = FetchWrapper(import.meta.env['VITE_LABWHERE_BASE_URL'], 'LabWhere')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Advisory: Creating the variables like this rather than using dependency injection makes it more difficult to test which is why you need to mock fetch globally I think!

A different way to do it would be to pass a type into the function which could have the various bits of configuration.

const destroyLocation = import.meta.env['VITE_DESTROYED_LOCATION_BARCODE']
/**
* Fetches the locations of labwares from LabWhere based on provided barcodes.
*
* @param {string[]} labwhereBarcodes - An array of labware barcodes to search for.
* @param {Object} [fetchWrapper=labwhereFetch] - The fetch wrapper to use for the request (optional).
* @returns {Promise<{success: boolean, errors: string[], data: Object}>} - A promise that resolves to an object containing the success status, any errors, and the data (locations).
*
* @example
Expand All @@ -18,7 +25,7 @@ const labwhereFetch = FetchWrapper(import.meta.env['VITE_LABWHERE_BASE_URL'], 'L
* }
* });
*/
const getLabwhereLocations = async (labwhereBarcodes) => {
const getLabwhereLocations = async (labwhereBarcodes, fetchWrapper = labwhereFetch) => {
// If no barcodes are provided, return a failed response.
if (!labwhereBarcodes || labwhereBarcodes.length === 0) {
return { success: false, errors: ['No barcodes provided'], data: {} }
Expand All @@ -28,7 +35,7 @@ const getLabwhereLocations = async (labwhereBarcodes) => {
params.append('barcodes[]', barcode)
})

const response = await labwhereFetch.post('/api/labwares/searches', params, 'multipart/form-data')
const response = await fetchWrapper.post('/api/labwares/searches', params, 'multipart/form-data')

if (response.success) {
response.data = extractLocationsForLabwares(response.data, labwhereBarcodes)
Expand All @@ -46,8 +53,9 @@ const getLabwhereLocations = async (labwhereBarcodes) => {
*
* @param {string} userCode - The user code or swipecard.
* @param {string} locationBarcode - The barcode of the location where labware will be stored.
* @param {string} labwareBarcodes - The barcodes of the labware to be stored, separated by newlines.
* @param {string} labwareBarcodes - The barcodes of the labware (library barcode or the plate / tube barcode for samples) to be stored, separated by newlines.
* @param {number|null} [startPosition=null] - The starting position for storing the labware (optional).
* @param {Object} [fetchWrapper=labwhereFetch] - The fetch wrapper to use for the request (optional).
* @returns {Promise<{success: boolean, errors: string[]}>} - A promise that resolves to an object containing the success status, any errors, and the data.
*
* @example
Expand All @@ -68,6 +76,7 @@ const scanBarcodesInLabwhereLocation = async (
locationBarcode,
labwareBarcodes,
startPosition,
fetchWrapper = labwhereFetch,
) => {
if (!userCode || !labwareBarcodes) {
return { success: false, errors: ['Required parameters are missing for the Scan In operation'] }
Expand All @@ -82,12 +91,61 @@ const scanBarcodesInLabwhereLocation = async (
if (startPosition) {
params['scan[start_position]'] = startPosition
}
const response = await labwhereFetch.post(
const response = await fetchWrapper.post(
'/api/scans',
new URLSearchParams(params).toString(),
'application/x-www-form-urlencoded',
)
return { success: response.success, errors: response.errors, message: response.data.message }
}
/**
* Exhausts the volume of libraries if the location barcode matches the destroy location.
*
* @param {string} locationBarcode - The barcode of the location.
* @param {Array<string>} labwareBarcodes - The barcodes of the labware.
* @returns {Promise<Object>} - An object containing the success status and the exhausted libraries.
*/
const exhaustLibraryVolumeIfDestroyed = async (locationBarcode, labwareBarcodes) => {
if (locationBarcode !== destroyLocation) return { success: false }
let librariesToDestroy = []

//Fetch libraries by filter key
const fetchAndMergeLibraries = async (barcodes, filterKey) => {
const filterOptions = { filter: { [filterKey]: barcodes.join(',') } }
seenanair marked this conversation as resolved.
Show resolved Hide resolved

const { success, libraries, tubes, tags, requests } =
await getPacbioLibraryResources(filterOptions)
if (success) {
librariesToDestroy = [
...librariesToDestroy,
...formatAndTransformLibraries(libraries, tubes, tags, requests),
]
}
}

export { getLabwhereLocations, scanBarcodesInLabwhereLocation }
//Fetch libraries by source_identifier
await fetchAndMergeLibraries(labwareBarcodes, 'source_identifier')
const barcodesNotFetchedAsSourceIdentifer = labwareBarcodes.filter(
(barcode) => !librariesToDestroy.some((library) => library.source_identifier === barcode),
)
// If not all libraries are found by source_identifier, try fetching by barcode
if (barcodesNotFetchedAsSourceIdentifer.length > 0) {
//Fetch libraries which are not found by barcode
await fetchAndMergeLibraries(barcodesNotFetchedAsSourceIdentifer, 'barcode')
}
// If no libraries are found, return a failed response
if (!librariesToDestroy.length) return { success: false }
const exhaustedLibraries = []

// Exhaust the volume of libraries
await Promise.all(
librariesToDestroy.map(async (library) => {
const { success } = await exhaustLibrayVolume(library)
if (success) {
exhaustedLibraries.push(library)
}
}),
)
return { success: exhaustedLibraries.length > 0, exhaustedLibraries }
}
export { getLabwhereLocations, scanBarcodesInLabwhereLocation, exhaustLibraryVolumeIfDestroyed }
103 changes: 103 additions & 0 deletions src/services/traction/PacbioLibrary.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { handleResponse } from '@/api/ResponseHelper.js'
import { groupIncludedByResource, dataToObjectById } from '@/api/JsonApi.js'
import useRootStore from '@/stores/index.js'

/**
* Fetches all libraries.
*
* @param {Object} getPacbioLibraryResources - The options to fetch libraries with.
* The options include page, filter, and include.
* e.g { page: { "size": "24", "number": "1"}, filter: { source_identifier: 'sample1' }, include: 'request,tag,tube' }
*/
async function getPacbioLibraryResources(fetchOptions = {}) {
const includes = new Set(fetchOptions.include ? fetchOptions.include.split(',') : [])
const requiredIncludes = ['request', 'tag', 'tube']
requiredIncludes.forEach((item) => includes.add(item))

const fetchOptionsDefaultInclude = {
...fetchOptions,
include: Array.from(includes).join(','),
}
const request = useRootStore().api.traction.pacbio.libraries
const response = await handleResponse(request.get(fetchOptionsDefaultInclude))

const { success, body: { data, included = [], meta = {} } = {}, errors = [] } = response
let libraries = {},
tubes = {},
tags = {},
requests = {}
if (success && data && data.length > 0) {
const {
tubes: included_tubes,
tags: included_tags,
requests: included_requests,
} = groupIncludedByResource(included)
libraries = dataToObjectById({ data, includeRelationships: true })
tubes = dataToObjectById({ data: included_tubes })
tags = dataToObjectById({ data: included_tags })
requests = dataToObjectById({ data: included_requests })
}
return { success, data, errors, meta, libraries, tubes, tags, requests }
}

/**
*
* @param {Integer | String} id - id of the library
* @param {Integer | String} pacbio_request_id - id of the pacbio request
* @param {String} template_prep_kit_box_barcode - barcode of the template prep kit box
* @param {Integer | String} tag_id - id of the tag
* @param {Float} concentration - concentration of the library
* @param {Float} volume - volume of the library
* @param {Float} insert_size - insert size of the library
* @returns {Object} - payload for creating a library
* if it is an update id is added otherwise pacbio_request_id is added
*
*/
const buildLibraryResourcePayload = ({
id,
pacbio_request_id,
template_prep_kit_box_barcode,
tag_id,
concentration,
volume,
insert_size,
}) => {
const requiredAttributes = {
template_prep_kit_box_barcode,
tag_id,
concentration,
volume,
insert_size,
}

const payload = {
data: {
type: 'libraries',
attributes: {
...requiredAttributes,
primary_aliquot_attributes: {
...requiredAttributes,
},
},
},
}

id ? (payload.data.id = id) : (payload.data.attributes.pacbio_request_id = pacbio_request_id)

return payload
}

/**
* Updates a library with the given fields and updates the store if successful.
*
* @param {Object} libraryFields - The fields of the library to update.
* @returns {Promise<Object>} - An object containing the success status and any errors.
*/
async function updatePacbioLibraryResource(libraryFields) {
const request = useRootStore().api.traction.pacbio.libraries
const promise = request.update(buildLibraryResourcePayload(libraryFields))
const { success, errors } = await handleResponse(promise)
return { success, errors }
}

export { getPacbioLibraryResources, updatePacbioLibraryResource, buildLibraryResourcePayload }
107 changes: 28 additions & 79 deletions src/stores/pacbioLibraries.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,16 @@
import { defineStore } from 'pinia'
import useRootStore from '@/stores'
import useRootStore from '@/stores/index.js'
import { handleResponse } from '@/api/ResponseHelper.js'
import { groupIncludedByResource, dataToObjectById } from '@/api/JsonApi.js'
import { groupIncludedByResource } from '@/api/JsonApi.js'
import { usePacbioRootStore } from '@/stores/pacbioRoot.js'
import { libraryPayload } from '@/stores//utilities/pacbioLibraries.js'

/**
* @function validateFields
* @param {Object} library - The library object to validate.
* @returns {boolean} Returns true if all required fields are present and truthy in the library object, false otherwise.
* @description Validates that the required fields are present in the given library object.
* The required fields are 'id', 'template_prep_kit_box_barcode', 'volume' and 'concentration'.
* The 'tag' and 'insert_size' fields are optional.
*/
const validateFields = (library) => {
const requiredAttributes = ['id', 'template_prep_kit_box_barcode', 'volume', 'concentration']
const errors = requiredAttributes.filter((field) => !library[field])

return {
success: errors.length === 0,
errors: 'Missing required field(s)',
}
}
import {
getPacbioLibraryResources,
buildLibraryResourcePayload,
} from '@/services/traction/PacbioLibrary.js'
import {
validateAndUpdateLibrary,
formatAndTransformLibraries,
} from '@/stores/utilities/pacbioLibraries.js'

/**
* Importing `defineStore` function from 'pinia' library.
Expand Down Expand Up @@ -60,37 +49,16 @@ export const usePacbioLibrariesStore = defineStore('pacbioLibraries', {
*/
librariesArray: (state) => {
const pacbioRootStore = usePacbioRootStore()
return Object.values(state.libraries)
.filter((library) => library.tube)
.map((library) => {
const { id, request, tag_id, tag, tube, ...attributes } = library
const tagId = tag_id ?? tag

/*Get the tag group ID from the library's tag ID or from the tag in pacbioRoot store(where all pacbio tags are kept). Why is this required?
/*Get the tag group ID from the library's tag ID or from the tag in pacbioRoot store(where all pacbio tags are kept). Why is this required?
The librariesArray is called in multiple places (in create and edit context) to get the libraries.
Therefore, librariesArray needs to search for the tag first in tags.
If not found, it should then look for it in 'pacbioRoot' store tags.
It's important to note that 'pacbioRoot' store tags will only get populated if a 'pacbioRoot' store action fetchPacbioTagSets is called before,
which may not happen in all the places where it's called.
Hence, a search in both places is required to ensure that librariesArray returns the correct tag
associated with all libraries."*/

const tagGroupId = state.tags[tagId]
? state.tags[tagId].group_id
: pacbioRootStore.tags[tagId]
? pacbioRootStore.tags[tagId].group_id
: ''

return {
id,
tag_id: String(tagId),
tube,
...attributes,
tag_group_id: tagGroupId ?? '',
sample_name: state.requests[request].sample_name,
barcode: state.tubes[tube].barcode,
}
})
const tags = { ...state.tags, ...pacbioRootStore.tags }
return formatAndTransformLibraries(state.libraries, state.tubes, tags, state.requests)
},
},
actions: {
Expand All @@ -113,7 +81,7 @@ export const usePacbioLibrariesStore = defineStore('pacbioLibraries', {
}) {
const rootState = useRootStore()
const request = rootState.api.traction.pacbio.libraries
const payload = libraryPayload({
const payload = buildLibraryResourcePayload({
pacbio_request_id,
template_prep_kit_box_barcode,
tag_id,
Expand Down Expand Up @@ -155,47 +123,28 @@ export const usePacbioLibrariesStore = defineStore('pacbioLibraries', {
* @param {number} page - The page number to fetch from the server.
* @returns {Promise<{success: boolean, errors: Array}>} - A promise that resolves to an object containing a success boolean and an array of errors.
*/
async fetchLibraries(filter = {}, page = {}) {
const rootStore = useRootStore()
const pacbioLibraries = rootStore.api.traction.pacbio.libraries
const promise = pacbioLibraries.get({
page,
filter,
include: 'request,tag,tube',
})
const response = await handleResponse(promise)

const { success, body: { data, included = [], meta = {} } = {}, errors = [] } = response

async fetchLibraries(filterOptions) {
const { success, data, meta, errors, libraries, tubes, tags, requests } =
await getPacbioLibraryResources({
...filterOptions,
})
if (success && data.length > 0) {
const { tubes, tags, requests } = groupIncludedByResource(included)
this.libraries = dataToObjectById({ data, includeRelationships: true })
this.tubes = dataToObjectById({ data: tubes })
this.tags = dataToObjectById({ data: tags })
this.requests = dataToObjectById({ data: requests })
this.libraries = libraries
this.tubes = tubes
this.tags = tags
this.requests = requests
}
return { success, errors, meta }
},

/**
* Updates the library with matchingid with all given field values.
* @param {*} libraryFields - The fields to update the library with.
* * @returns {Promise<Object>} A promise that resolves to an object.
* The object has a 'success' property that is true if the library was updated successfully and false otherwise.
* If 'success' is false, the object also has an 'errors' property with a message describing the error.
* Updates a library with the given fields and updates the store if successful.
*
* @param {Object} libraryFields - The fields of the library to update.
* @returns {Promise<Object>} - An object containing the success status and any errors.
*/
async updateLibrary(libraryFields) {
//Validate the libraryFields to ensure that all required fields are present
const valid = validateFields(libraryFields)
if (!valid.success) {
return { success: false, errors: valid.errors }
}

const rootStore = useRootStore()
const request = rootStore.api.traction.pacbio.libraries
const payload = libraryPayload(libraryFields)
const promise = request.update(payload)
const { success, errors } = await handleResponse(promise)
const { success, errors } = await validateAndUpdateLibrary(libraryFields)
if (success) {
//Update all fields of the library in the store with matching ID with the given values.
this.libraries[libraryFields.id] = {
Expand Down
Loading
Loading