Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { StudyModule } from 'src/study/study.module';
import { StudyDownloadRequestResolver } from './resolvers/study-download-request.resolver';
import { StudyDownloadService } from './services/study-download-request.service';
import { TagModule } from '../tag/tag.module';
import { StudyDownloadRequestPipe } from './pipes/study-download-request.pipe';
import { DatasetDownloadRequestPipe } from './pipes/dataset-download-request.pipe';

@Module({
imports: [
Expand All @@ -38,7 +40,9 @@ import { TagModule } from '../tag/tag.module';
CreateDatasetDownloadPipe,
CreateStudyDownloadPipe,
StudyDownloadRequestResolver,
StudyDownloadService
StudyDownloadService,
StudyDownloadRequestPipe,
DatasetDownloadRequestPipe
]
})
export class DownloadRequestModule {}
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { Schema, Prop, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
import { DownloadStatus, DownloadRequest } from './download-request.model';
import { ObjectType, Field } from '@nestjs/graphql';
import { ObjectType, Field, registerEnumType } from '@nestjs/graphql';

export enum DatasetDownloadField {
ENTRY_ZIP = 'ENTRY_ZIP'
}

registerEnumType(DatasetDownloadField, {
name: 'DatasetDownloadField'
});

@Schema()
@ObjectType()
Expand Down Expand Up @@ -34,6 +42,12 @@ export class DatasetDownloadRequest implements DownloadRequest {

@Prop({ required: false })
webhookPayloadLocation?: string;

@Prop({ required: true })
entryZipComplete: boolean;

@Prop({ required: true })
verificationCode: string;
}

export type DatasetDownloadRequestDocument = Document & DatasetDownloadRequest;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import { Schema, Prop, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
import { DownloadRequest, DownloadStatus } from './download-request.model';
import { Field, ObjectType } from '@nestjs/graphql';
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';

export enum StudyDownloadField {
ENTRY_ZIP = 'ENTRY_ZIP',
TAGGED_ENTRIES_ZIP = 'TAGGED_ENTRIES_ZIP'
}

registerEnumType(StudyDownloadField, {
name: 'StudyDownloadField'
});

@Schema()
@ObjectType()
Expand Down Expand Up @@ -54,6 +63,15 @@ export class StudyDownloadRequest implements DownloadRequest {
/** Webhook payload to be used when the zipping of tagged entries is complete */
@Prop({ required: false })
taggedEntryWebhookPayloadLocation?: string;

@Prop({ required: true })
entryZipComplete: boolean;

@Prop({ required: true })
taggedEntryZipComplete: boolean;

@Prop({ required: true })
verificationCode: string;
}

export type StudyDownloadRequestDocument = Document & StudyDownloadRequest;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Injectable, PipeTransform, BadRequestException } from '@nestjs/common';
import { DatasetDownloadRequest } from '../models/dataset-download-request.model';
import { DatasetDownloadService } from '../services/dataset-download-request.service';

@Injectable()
export class DatasetDownloadRequestPipe implements PipeTransform<string, Promise<DatasetDownloadRequest>> {
constructor(private readonly downloadRequestService: DatasetDownloadService) {}

async transform(value: string): Promise<DatasetDownloadRequest> {
const downloadRequest = await this.downloadRequestService.find(value);
if (!downloadRequest) {
throw new BadRequestException(`Dataset download request with id ${value} not found`);
}
return downloadRequest;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { BadRequestException, Injectable, PipeTransform } from '@nestjs/common';
import { StudyDownloadRequest } from '../models/study-download-request.model';
import { StudyDownloadService } from '../services/study-download-request.service';

@Injectable()
export class StudyDownloadRequestPipe implements PipeTransform<string, Promise<StudyDownloadRequest>> {
constructor(private readonly downloadService: StudyDownloadService) {}

async transform(value: string): Promise<StudyDownloadRequest> {
const downloadRequest = await this.downloadService.find(value);
if (!downloadRequest) {
throw new BadRequestException(`Study down with the id ${value} not found`);
}
return downloadRequest;
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { Resolver, Mutation, Args, Query, ID, ResolveField, Parent } from '@nestjs/graphql';
import { CreateDatasetDownloadRequest } from '../dtos/dataset-download-request-create.dto';
import { DatasetDownloadRequest } from '../models/dataset-download-request.model';
import { DatasetDownloadRequest, DatasetDownloadField } from '../models/dataset-download-request.model';
import { DatasetDownloadService } from '../services/dataset-download-request.service';
import { UseGuards } from '@nestjs/common';
import { UnauthorizedException, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../../jwt/jwt.guard';
import { OrganizationContext } from 'src/organization/organization.context';
import { Organization } from '../../organization/organization.model';
import { OrganizationGuard } from '../../organization/organization.guard';
import { CreateDatasetDownloadPipe } from '../pipes/dataset-download-request-create.pipe';
import { Dataset } from '../../dataset/dataset.model';
import { DatasetPipe } from '../../dataset/pipes/dataset.pipe';
import { DatasetDownloadRequestPipe } from '../pipes/dataset-download-request.pipe';

@UseGuards(JwtAuthGuard, OrganizationGuard)
@Resolver(() => DatasetDownloadRequest)
Expand All @@ -34,6 +35,20 @@ export class DatasetDownloadRequestResolver {
return this.datasetDownloadService.getDatasetDownloadRequests(dataset);
}

@Mutation(() => Boolean)
async markDatasetFieldComplete(
@Args('downloadRequest', { type: () => ID }, DatasetDownloadRequestPipe) downloadRequest: DatasetDownloadRequest,
@Args('datasetField', { type: () => DatasetDownloadField }) datasetField: DatasetDownloadField,
@Args('code') verificationCode: string
): Promise<boolean> {
if (verificationCode !== downloadRequest.verificationCode) {
throw new UnauthorizedException(`Invalid verification code`);
}

await this.datasetDownloadService.markFieldComplete(downloadRequest, datasetField);
return true;
}

@ResolveField(() => String)
async entryZip(@Parent() downloadRequest: DatasetDownloadRequest): Promise<string> {
return this.datasetDownloadService.getEntryZipURL(downloadRequest);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { Resolver, Mutation, Args, ResolveField, Parent, ID, Query } from '@nestjs/graphql';
import { JwtAuthGuard } from '../../jwt/jwt.guard';
import { OrganizationGuard } from '../../organization/organization.guard';
import { UseGuards } from '@nestjs/common';
import { StudyDownloadRequest } from '../models/study-download-request.model';
import { UnauthorizedException, UseGuards } from '@nestjs/common';
import { StudyDownloadField, StudyDownloadRequest } from '../models/study-download-request.model';
import { StudyDownloadService } from '../services/study-download-request.service';
import { CreateStudyDownloadPipe } from '../pipes/study-download-request-create.pipe';
import { CreateStudyDownloadRequest } from '../dtos/study-download-request-create.dto';
import { OrganizationContext } from '../../organization/organization.context';
import { Organization } from '../../organization/organization.model';
import { StudyPipe } from '../../study/pipes/study.pipe';
import { Study } from '../../study/study.model';
import { StudyDownloadRequestPipe } from '../pipes/study-download-request.pipe';

@UseGuards(JwtAuthGuard, OrganizationGuard)
@Resolver(() => StudyDownloadRequest)
Expand All @@ -29,6 +30,20 @@ export class StudyDownloadRequestResolver {
return this.studyDownloadService.getStudyDownloads(study);
}

@Mutation(() => Boolean)
async markStudyFieldComplete(
@Args('downloadRequest', { type: () => ID }, StudyDownloadRequestPipe) downloadRequest: StudyDownloadRequest,
@Args('studyField', { type: () => StudyDownloadField }) studyField: StudyDownloadField,
@Args('code') verificationCode: string
): Promise<boolean> {
if (downloadRequest.verificationCode !== verificationCode) {
throw new UnauthorizedException('Invalid verification code');
}

await this.studyDownloadService.markStudyFieldComplete(downloadRequest, studyField);
return true;
}

@ResolveField(() => String)
async entryZip(@Parent() downloadRequest: StudyDownloadRequest): Promise<string> {
return this.studyDownloadService.getEntryZipUrl(downloadRequest);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import { BucketFactory } from '../../bucket/bucket-factory.service';
import { EntryService } from '../../entry/services/entry.service';
import { Organization } from '../../organization/organization.model';
import { CreateDatasetDownloadRequest } from '../dtos/dataset-download-request-create.dto';
import { DatasetDownloadRequest } from '../models/dataset-download-request.model';
import { DatasetDownloadField, DatasetDownloadRequest } from '../models/dataset-download-request.model';
import { DownloadRequest, DownloadStatus } from '../models/download-request.model';
import { DownloadRequestService } from './download-request.service';
import { randomUUID } from 'crypto';

@Injectable()
export class DatasetDownloadService {
Expand All @@ -33,7 +34,9 @@ export class DatasetDownloadService {
...downloadRequest,
date: new Date(),
status: DownloadStatus.IN_PROGRESS,
organization: organization._id
organization: organization._id,
entryZipComplete: false,
verificationCode: randomUUID()
});

const bucketLocation = `${this.downloadService.getPrefix()}/${request._id}`;
Expand Down Expand Up @@ -88,4 +91,21 @@ export class DatasetDownloadService {
new Date(Date.now() + this.expiration)
);
}

async find(id: string): Promise<DatasetDownloadRequest | null> {
return this.downloadRequestModel.findById({ _id: id });
}

async markFieldComplete(downloadRequest: DatasetDownloadRequest, field: DatasetDownloadField): Promise<void> {
switch (field) {
case DatasetDownloadField.ENTRY_ZIP:
await this.downloadRequestModel.updateOne({ _id: downloadRequest._id }, { $set: { entryZipComplete: true } });
break;
default:
throw new Error(`Unknown dataset download field ${field}`);
}

// With only one field supported, can mark the download as complete
await this.downloadRequestModel.updateOne({ _id: downloadRequest._id }, { $set: { status: DownloadStatus.READY } });
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common';
import { StudyDownloadRequest } from '../models/study-download-request.model';
import { StudyDownloadField, StudyDownloadRequest } from '../models/study-download-request.model';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { CreateStudyDownloadRequest } from '../dtos/study-download-request-create.dto';
Expand All @@ -15,6 +15,7 @@ import { VideoFieldService } from '../../tag/services/video-field.service';
import { BucketObjectAction } from 'src/bucket/bucket';
import { Entry } from 'src/entry/models/entry.model';
import { Study } from 'src/study/study.model';
import { randomUUID } from 'crypto';

@Injectable()
export class StudyDownloadService {
Expand All @@ -39,7 +40,10 @@ export class StudyDownloadService {
...downloadRequest,
date: new Date(),
status: DownloadStatus.IN_PROGRESS,
organization: organization._id
organization: organization._id,
entryZipComplete: false,
taggedEntryZipComplete: false,
verificationCode: randomUUID()
});

const bucketLocation = `${this.downloadService.getPrefix()}/${request._id}`;
Expand Down Expand Up @@ -102,6 +106,39 @@ export class StudyDownloadService {
return this.downloadRequestModel.find({ study: study._id });
}

async find(id: string): Promise<StudyDownloadRequest | null> {
return this.downloadRequestModel.findById(id);
}

/**
* Handles flagging when a field is complete and then updating the status when all fields are complete
*/
async markStudyFieldComplete(downloadRequest: StudyDownloadRequest, studyField: StudyDownloadField): Promise<void> {
// Mark the field as complete
switch (studyField) {
case StudyDownloadField.ENTRY_ZIP:
console.log('here');
await this.downloadRequestModel.updateOne({ _id: downloadRequest._id }, { $set: { entryZipComplete: true } });
break;
case StudyDownloadField.TAGGED_ENTRIES_ZIP:
await this.downloadRequestModel.updateOne(
{ _id: downloadRequest._id },
{ $set: { taggedEntryZipComplete: true } }
);
break;
default:
throw new Error(`Unknown field ${studyField}`);
}

const request = (await this.downloadRequestModel.findOne({ _id: downloadRequest._id }))!;

// Check if all components are complete
if (request.taggedEntryZipComplete && request.entryZipComplete) {
// Mark as complete
await this.downloadRequestModel.updateOne({ _id: request._id }, { $set: { status: DownloadStatus.READY } });
}
}

/**
* Handles generating the CSV for the tag data. This approach is a sub-optimal one.
*
Expand Down