diff --git a/src/domain/entities/sample.ts b/src/domain/entities/sample.ts new file mode 100644 index 0000000..baebbd5 --- /dev/null +++ b/src/domain/entities/sample.ts @@ -0,0 +1,51 @@ + +export interface PublicSampleResponseModel { + sample_name: string, + raw_file_name: string, + station_id: string, + first_image: string, + last_image: string, + comment: string, + qc_lvl1: boolean, + qc_lvl1_comment: string, +} + +export interface PublicHeaderSampleResponseModel { + sample_name: string, + raw_file_name: string, + station_id: string, + first_image: string, + last_image: string, + comment: string, + qc_lvl1: boolean, + qc_lvl1_comment: string, +} + +export interface HeaderSampleModel { + cruise: string; + ship: string; + filename: string; + profileId: string; + bottomDepth: string; + ctdRosetteFilename: string; + latitude: string; + longitude: string; + firstImage: string; + volImage: string; + aa: string; + exp: string; + dn: string; + windDir: string; + windSpeed: string; + seaState: string; + nebulousness: string; + comment: string; + endImg: string; + yoyo: string; + stationId: string; + sampleType: string; + integrationTime: string; + argoId: string; + pixelSize: string; + sampleDateTime: string; +} \ No newline at end of file diff --git a/src/domain/interfaces/repositories/sample-repository.ts b/src/domain/interfaces/repositories/sample-repository.ts new file mode 100644 index 0000000..65db504 --- /dev/null +++ b/src/domain/interfaces/repositories/sample-repository.ts @@ -0,0 +1,6 @@ +import { PublicSampleResponseModel } from "../../entities/sample"; + +export interface SampleRepository { + ensureFolderExists(root_folder_path: string): Promise; + listImportableSamples(root_folder_path: string): Promise; +} \ No newline at end of file diff --git a/src/domain/interfaces/use-cases/sample/list-importable-samples.ts b/src/domain/interfaces/use-cases/sample/list-importable-samples.ts new file mode 100644 index 0000000..777d1db --- /dev/null +++ b/src/domain/interfaces/use-cases/sample/list-importable-samples.ts @@ -0,0 +1,6 @@ +import { PublicSampleResponseModel } from "../../../entities/sample"; +import { UserUpdateModel } from "../../../entities/user"; + +export interface ListImportableSamplesUseCase { + execute(current_user: UserUpdateModel, project_id: number): Promise; +} \ No newline at end of file diff --git a/src/domain/repositories/sample-repository.ts b/src/domain/repositories/sample-repository.ts new file mode 100644 index 0000000..ef59359 --- /dev/null +++ b/src/domain/repositories/sample-repository.ts @@ -0,0 +1,244 @@ + +// import { SampleDataSource } from "../../data/interfaces/data-sources/sample-data-source"; +// import { InstrumentModelResponseModel } from "../entities/instrument_model"; +// import { PublicPrivilege } from "../entities/privilege"; +// import { SampleRequestCreationModel, SampleRequestModel, SampleUpdateModel, SampleResponseModel, PublicSampleResponseModel, PublicSampleRequestCreationModel } from "../entities/sample"; +// import { PreparedSearchOptions, SearchResult } from "../entities/search"; +// import { SampleRepository } from "../interfaces/repositories/sample-repository"; + +import { head } from "shelljs"; +import { HeaderSampleModel, PublicHeaderSampleResponseModel } from "../entities/sample"; +import { SampleRepository } from "../interfaces/repositories/sample-repository"; + + +import { promises as fs } from 'fs'; +import path from 'path'; + +export class SampleRepositoryImpl implements SampleRepository { + + //sampleDataSource: SampleDataSource + + // // TODO move to a search repository + // order_by_allow_params: string[] = ["asc", "desc"] + // filter_operator_allow_params: string[] = ["=", ">", "<", ">=", "<=", "<>", "IN", "LIKE"] + + // constructor(sampleDataSource: SampleDataSource) { + // this.sampleDataSource = sampleDataSource + // } + + async ensureFolderExists(root_folder_path: string): Promise { + const folderPath = path.join(root_folder_path); + + try { + await fs.access(folderPath); + console.log('Folder exists'); + } catch (error) { + throw new Error(`Folder does not exist at path: ${folderPath}`); + } + } + + async listImportableSamples(root_folder_path: string): Promise { + const folderPath = path.join(root_folder_path); + // read from folderPath/meta/*header*.txt and return the list of samples + const meta_header_samples = await this.getSamplesFromHeaders(folderPath); + + // read from folderPath/ecodata and return the list of samples + const ecodata_samples = await this.getSamplesFromEcodata(folderPath); + + // flag qc samples to flase if not in both lists, and add qc message + const samples: PublicHeaderSampleResponseModel[] = []; + for (const sample of meta_header_samples) { + samples.push({ + sample_name: sample.filename, + raw_file_name: sample.filename, + station_id: sample.stationId, + first_image: sample.firstImage, + last_image: sample.endImg, + comment: sample.comment, + qc_lvl1: ecodata_samples.includes(sample.filename) ? true : false, + qc_lvl1_comment: ecodata_samples.includes(sample.filename) ? '' : 'Sample not found in ecodata folder' + }); + } + return samples; + } + + // Function to read and return samples from header.txt files + async getSamplesFromHeaders(folderPath: string): Promise { + const samples: HeaderSampleModel[] = []; + try { + const header_path = path.join(folderPath, 'meta'); + const files = await fs.readdir(header_path); + console.log('header files', files); + for (const file of files) { + if (file.includes('header') && file.endsWith('.txt')) { + const filePath = path.join(header_path, file); + const content = await fs.readFile(filePath, 'utf8'); + + const lines = content.trim().split('\n'); + for (let i = 1; i < lines.length; i++) { + samples.push(this.getSampleFromHeaderLine(lines[i])); + } + } + } + } catch (err) { + throw new Error(`Error reading files: ${err.message}`); + } + + return samples; + } + + getSampleFromHeaderLine(line: string): HeaderSampleModel { + console.log('line', line); + const fields = line.split(';'); + + const sample: HeaderSampleModel = { + cruise: fields[0], + ship: fields[1], + filename: fields[2], + profileId: fields[3], + bottomDepth: fields[4], + ctdRosetteFilename: fields[5], + latitude: fields[6], + longitude: fields[7], + firstImage: fields[8], + volImage: fields[9], + aa: fields[10], + exp: fields[11], + dn: fields[12], + windDir: fields[13], + windSpeed: fields[14], + seaState: fields[15], + nebulousness: fields[16], + comment: fields[17], + endImg: fields[18], + yoyo: fields[19], + stationId: fields[20], + sampleType: fields[21], + integrationTime: fields[22], + argoId: fields[23], + pixelSize: fields[24], + sampleDateTime: fields[25] + }; + return sample; + } + + // Function to read and return samples from ecodata folder names + async getSamplesFromEcodata(folderPath: string): Promise { + const samples: string[] = []; + try { + const files = await fs.readdir(path.join(folderPath, 'ecodata')); + + for (const file of files) { + samples.push(file); + } + } catch (err) { + throw new Error(`Error reading files: ${err.message}`); + } + + return samples; + } + + + + // async createSample(sample: SampleRequestCreationModel): Promise { + // const result = await this.sampleDataSource.create(sample) + // return result; + // } + + // async getSample(sample: SampleRequestModel): Promise { + // const result = await this.sampleDataSource.getOne(sample) + // return result; + // } + + // async deleteSample(sample: SampleRequestModel): Promise { + // const result = await this.sampleDataSource.deleteOne(sample) + // return result; + // } + + // private async updateSample(sample: SampleUpdateModel, params: string[]): Promise { + // const filteredSample: Partial = {}; + // const unauthorizedParams: string[] = []; + + // // Filter the sample object based on authorized parameters + // Object.keys(sample).forEach(key => { + // if (key === 'sample_id') { + // filteredSample[key] = sample[key]; + // } else if (params.includes(key)) { + // filteredSample[key] = sample[key]; + // } else { + // unauthorizedParams.push(key); + // } + // }); + + // // If unauthorized params are found, throw an error + // if (unauthorizedParams.length > 0) { + // throw new Error(`Unauthorized or unexisting parameters : ${unauthorizedParams.join(', ')}`); + // } + // // If there are valid parameters, update the sample + // if (Object.keys(filteredSample).length <= 1) { + // throw new Error('Please provide at least one valid parameter to update'); + // } + // const updatedSampleCount = await this.sampleDataSource.updateOne(filteredSample as SampleUpdateModel); + // return updatedSampleCount; + // } + + // async standardUpdateSample(sample: SampleUpdateModel): Promise { + // const params_restricted = ["sample_id", "root_folder_path", "sample_title", "sample_acronym", "sample_description", "sample_information", "cruise", "ship", "data_owner_name", "data_owner_email", "operator_name", "operator_email", "chief_scientist_name", "chief_scientist_email", "override_depth_offset", "enable_descent_filter", "privacy_duration", "visible_duration", "public_duration", "instrument_model", "serial_number"] + // const updated_sample_nb = await this.updateSample(sample, params_restricted) + // return updated_sample_nb + // } + + // async standardGetSamples(options: PreparedSearchOptions): Promise> { + // // Can be filtered by + // const filter_params_restricted = ["sample_id", "root_folder_path", "sample_title", "sample_acronym", "sample_description", "sample_information", "cruise", "ship", "data_owner_name", "data_owner_email", "operator_name", "operator_email", "chief_scientist_name", "chief_scientist_email", "override_depth_offset", "enable_descent_filter", "privacy_duration", "visible_duration", "public_duration", "instrument_model", "serial_number", "sample_creation_date"] + + // // Can be sort_by + // const sort_param_restricted = ["sample_id", "root_folder_path", "sample_title", "sample_acronym", "sample_description", "sample_information", "cruise", "ship", "data_owner_name", "data_owner_email", "operator_name", "operator_email", "chief_scientist_name", "chief_scientist_email", "override_depth_offset", "enable_descent_filter", "privacy_duration", "visible_duration", "public_duration", "instrument_model", "serial_number", "sample_creation_date"] + + // return await this.getSamples(options, filter_params_restricted, sort_param_restricted, this.order_by_allow_params, this.filter_operator_allow_params) + // } + + // //TODO MOVE TO SEARCH REPOSITORY + // private async getSamples(options: PreparedSearchOptions, filtering_params: string[], sort_by_params: string[], order_by_params: string[], filter_operator_params: string[]): Promise> { + // const unauthorizedParams: string[] = []; + // //TODO move to a search repository + // // Filter options.sort_by by sorting params + // options.sort_by = options.sort_by.filter(sort_by => { + // let is_valid = true; + // if (!sort_by_params.includes(sort_by.sort_by)) { + // unauthorizedParams.push(`Unauthorized sort_by: ${sort_by.sort_by}`); + // is_valid = false; + // } + // if (!order_by_params.includes(sort_by.order_by)) { + // unauthorizedParams.push(`Unauthorized order_by: ${sort_by.order_by}`); + // is_valid = false; + // } + // return is_valid; + // }); + + // //TODO move to a search repository + // // Filter options.filters by filtering params + // options.filter = options.filter.filter(filter => { + // let is_valid = true; + // if (!filtering_params.includes(filter.field)) { + // unauthorizedParams.push(`Filter field: ${filter.field}`); + // is_valid = false; + // } + // if (!filter_operator_params.includes(filter.operator)) { + // unauthorizedParams.push(`Filter operator: ${filter.operator}`); + // is_valid = false; + // } + // return is_valid; + // }); + + // //TODO move to a search repository + // if (unauthorizedParams.length > 0) { + // throw new Error(`Unauthorized or unexisting parameters : ${unauthorizedParams.join(', ')}`); + // } + + // return await this.sampleDataSource.getAll(options); + // } + + + +} \ No newline at end of file diff --git a/src/domain/use-cases/sample/list-importable-samples.ts b/src/domain/use-cases/sample/list-importable-samples.ts new file mode 100644 index 0000000..f3acf27 --- /dev/null +++ b/src/domain/use-cases/sample/list-importable-samples.ts @@ -0,0 +1,70 @@ +import { PublicSampleResponseModel } from "../../entities/sample"; +import { UserUpdateModel } from "../../entities/user"; +import { PrivilegeRepository } from "../../interfaces/repositories/privilege-repository"; +import { SampleRepository } from "../../interfaces/repositories/sample-repository"; +import { ProjectRepository } from "../../interfaces/repositories/project-repository"; +import { UserRepository } from "../../interfaces/repositories/user-repository"; + +import { ListImportableSamplesUseCase } from "../../interfaces/use-cases/sample/list-importable-samples"; +import { ProjectResponseModel } from "../../entities/project"; + +export class ListImportableSamples implements ListImportableSamplesUseCase { + sampleRepository: SampleRepository + userRepository: UserRepository + privilegeRepository: PrivilegeRepository + projectRepository: ProjectRepository + + constructor(sampleRepository: SampleRepository, userRepository: UserRepository, privilegeRepository: PrivilegeRepository, projectRepository: ProjectRepository) { + this.sampleRepository = sampleRepository + this.userRepository = userRepository + this.privilegeRepository = privilegeRepository + this.projectRepository = projectRepository + } + + async execute(current_user: UserUpdateModel, project_id: number): Promise { + // Ensure the user is valid and can be used + await this.userRepository.ensureUserCanBeUsed(current_user.user_id); + + // Ensure the current user has permission to get the project importable samples + await this.ensureUserCanGet(current_user, project_id); + + const project: ProjectResponseModel = await this.getProjectIfExist(project_id); + + const samples = await this.listImportableSamples(project); + + // Ensure the task to get exists + if (!samples) { throw new Error("Cannot find samples"); } + + return samples; + } + + private async listImportableSamples(project: ProjectResponseModel): Promise { + await this.sampleRepository.ensureFolderExists(project.root_folder_path); + const samples = await this.sampleRepository.listImportableSamples(project.root_folder_path); + return samples; + } + + private async getProjectIfExist(project_id: number): Promise { + const project = await this.projectRepository.getProject({ project_id: project_id }); + if (!project) { + throw new Error("Cannot find project"); + } + return project; + } + + private async ensureUserCanGet(current_user: UserUpdateModel, project_id: number): Promise { + console.log("project_id : ", project_id); + console.log("current_user.user_id : ", current_user.user_id); + const userIsAdmin = await this.userRepository.isAdmin(current_user.user_id); + console.log("userIsAdmin : ", userIsAdmin); + const userHasPrivilege = await this.privilegeRepository.isGranted({ + user_id: current_user.user_id, + project_id: project_id + }); + console.log("userHasPrivilege : ", userHasPrivilege); + + if (!userIsAdmin && !userHasPrivilege) { + throw new Error("Logged user cannot list importable samples in this project"); + } + } +} \ No newline at end of file diff --git a/src/presentation/routers/project-router.ts b/src/presentation/routers/project-router.ts index f976231..d53c14c 100644 --- a/src/presentation/routers/project-router.ts +++ b/src/presentation/routers/project-router.ts @@ -10,6 +10,7 @@ import { UpdateProjectUseCase } from '../../domain/interfaces/use-cases/project/ import { CustomRequest } from '../../domain/entities/auth' import { SearchProjectsUseCase } from '../../domain/interfaces/use-cases/project/search-project' +import { ListImportableSamplesUseCase } from '../../domain/interfaces/use-cases/sample/list-importable-samples' export default function ProjectRouter( middlewareAuth: MiddlewareAuth, @@ -17,7 +18,8 @@ export default function ProjectRouter( createProjectUseCase: CreateProjectUseCase, deleteProjectUseCase: DeleteProjectUseCase, updateProjectUseCase: UpdateProjectUseCase, - searchProjectUseCase: SearchProjectsUseCase + searchProjectUseCase: SearchProjectsUseCase, + listImportableSamples: ListImportableSamplesUseCase, ) { const router = express.Router() @@ -121,5 +123,36 @@ export default function ProjectRouter( } }) + /***********************************************SAMPLES***********************************************/ + + router.get('/:project_id/samples/can_be_imported', middlewareAuth.auth, async (req: Request, res: Response) => { + try { + const tasks = await listImportableSamples.execute((req as CustomRequest).token, req.params.project_id as any); + res.status(200).send(tasks) + } catch (err) { + console.log(err) + // if (err.message === "User cannot be used") res.status(403).send({ errors: [err.message] }) + // else if (err.message === "Task type label not found") res.status(404).send({ errors: [err.message] }) + // else if (err.message === "Task status label not found") res.status(404).send({ errors: [err.message] }) + // else res.status(500).send({ errors: ["Cannot search tasks"] }) + res.status(500).send({ errors: ["Cannot search tasks"] }) + } + }) + + // // Pagined and sorted list of filtered task + // router.post('/:project_id/samples/import', middlewareAuth.auth,/*middlewareSampleValidation.rulesImport,*/ async (req: Request, res: Response) => { + // try { + // const tasks = await importSamples.execute((req as CustomRequest).token, { ...req.body, project_id: req.params.project_id }); + // res.status(200).send(tasks) + // } catch (err) { + // // console.log(err) + // // if (err.message === "User cannot be used") res.status(403).send({ errors: [err.message] }) + // // else if (err.message === "Task type label not found") res.status(404).send({ errors: [err.message] }) + // // else if (err.message === "Task status label not found") res.status(404).send({ errors: [err.message] }) + // // else res.status(500).send({ errors: ["Cannot search tasks"] }) + // res.status(500).send({ errors: ["Cannot search tasks"] }) + // } + // }) + return router } \ No newline at end of file