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

Clear clinical submission resolver #1055

Merged
merged 7 commits into from
Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from 6 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
8 changes: 8 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ process.title = 'clinical';

// Create Express server
const app = express();

export type GlobalGqlContext = {
isUserRequest: boolean;
egoToken: string;
Authorization: string;
dataLoaders: {};
};

app.set('port', process.env.PORT || 3000);
app.set('graphqlPort', process.env.GRAPHQLPORT || 3001);
app.use(bodyParser.json());
Expand Down
4 changes: 2 additions & 2 deletions src/dictionary/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class SchemaController {
async update(req: Request, res: Response) {
const version: string = req.body.version;
const sync: boolean = req.query.sync;
const initiator = ControllerUtils.getUserFromToken(req);
const initiator = ControllerUtils.getUserFromRequest(req);
const migration = await manager.instance().updateSchemaVersion(version, initiator, sync);
return res.status(200).send(migration);
}
Expand All @@ -49,7 +49,7 @@ class SchemaController {
@HasFullWriteAccess()
async dryRunUpdate(req: Request, res: Response) {
const version: string = req.body.version;
const initiator = ControllerUtils.getUserFromToken(req);
const initiator = ControllerUtils.getUserFromRequest(req);
const migration = await manager.instance().dryRunSchemaUpgrade(version, initiator);
return res.status(200).send(migration);
}
Expand Down
43 changes: 43 additions & 0 deletions src/schemas/clinical-resolvers/clearClinicalSubmissionResolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright (c) 2023 The Ontario Institute for Cancer Research. All rights reserved
*
* This program and the accompanying materials are made available under the terms of
* the GNU Affero General Public License v3.0. You should have received a copy of the
* GNU Affero General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
* SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
* IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

import { GlobalGqlContext } from '../../app';
import submissionAPI from '../../submission/submission-api';
import { convertClinicalSubmissionDataToGql } from '../utils';

const clearClinicalSubmissionResolver = {
clearClinicalSubmission: async (
obj: unknown,
args: { programShortName: string; fileType: string; version: string },
contextValue: any,
) => {
const { programShortName, fileType, version } = args;
const response = await submissionAPI.clearFileDataFromActiveSubmission(
programShortName,
fileType,
version,
(<GlobalGqlContext>contextValue).egoToken,
Copy link
Contributor

Choose a reason for hiding this comment

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

For consistency w/ other functions I would change line 28 to contextValue: GlobalGqlContext, and
line 35 to contextValue.egoToken,

Copy link
Contributor Author

Choose a reason for hiding this comment

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

unfortunately, the buildSubgraphSchema method does not accept resolvers of custom context type. Hence the typecast.

Copy link
Contributor

Choose a reason for hiding this comment

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

OK I see now, sorry VSCode TS version was not set correctly

);
return convertClinicalSubmissionDataToGql(programShortName, {
submission: response,
});
},
};

export default clearClinicalSubmissionResolver;
33 changes: 1 addition & 32 deletions src/schemas/clinical-resolvers/clinicalSubmissionDataResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import submissionAPI from '../../submission/submission-api';
import get from 'lodash/get';
import { ActiveClinicalSubmission } from '../../submission/submission-entities';
import { DeepReadonly } from 'deep-freeze';
import { convertClinicalFileErrorToGql, convertClinicalSubmissionEntityToGql } from '../utils';
import { convertClinicalSubmissionDataToGql } from '../utils';
import { getClinicalEntitiesData } from '../../dictionary/api';

const clinicalSubmissionResolver = {
Expand All @@ -35,35 +35,4 @@ const clinicalSubmissionResolver = {
},
};

const convertClinicalSubmissionDataToGql = (
programShortName: string,
data: {
submission: DeepReadonly<ActiveClinicalSubmission> | undefined;
batchErrors?: { message: string; batchNames: string[]; code: string }[];
},
) => {
const submission = get(data, 'submission', {} as Partial<typeof data.submission>);
const fileErrors = get(data, 'batchErrors', [] as typeof data.batchErrors);
const clinicalEntities = get(submission, 'clinicalEntities');
return {
id: submission?._id || undefined,
programShortName,
state: submission?.state || undefined,
version: submission?.version || undefined,
updatedBy: submission?.updatedBy || undefined,
updatedAt: submission?.updatedAt ? submission.updatedAt : undefined,
clinicalEntities: async () => {
const clinicalSubmissionTypeList = await getClinicalEntitiesData('false'); // to confirm for true or false
const filledClinicalEntities = clinicalSubmissionTypeList.map(clinicalType => ({
clinicalType,
...(clinicalEntities ? clinicalEntities[clinicalType.name] : {}),
}));
return filledClinicalEntities.map(clinicalEntity =>
convertClinicalSubmissionEntityToGql(clinicalEntity?.clinicalType.name, clinicalEntity),
);
},
fileErrors: fileErrors?.map(convertClinicalFileErrorToGql),
};
};

export default clinicalSubmissionResolver;
12 changes: 12 additions & 0 deletions src/schemas/gqlTypeDefs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ const typeDefs = gql`
clinicalSubmissions(programShortName: String!): ClinicalSubmissionData!
}

type Mutation {
"""
Clear Clinical Submission
fileType is optional, if it is not provided all fileTypes will be cleared. The values for fileType are the same as the file names from each template (ex. donor, specimen)
"""
clearClinicalSubmission(
programShortName: String!
version: String!
fileType: String
): ClinicalSubmissionData!
}

scalar DateTime

"""
Expand Down
4 changes: 4 additions & 0 deletions src/schemas/resolvers.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import clinicalRegistrationResolver from './clinical-resolvers/clinicalRegistrationData';
import clinicalSearchResultResolver from './clinical-resolvers/clinicalSearchResults';
import clinicalSubmissionResolver from './clinical-resolvers/clinicalSubmissionDataResolver';
import clearClinicalSubmissionResolver from './clinical-resolvers/clearClinicalSubmissionResolver';

const resolvers = {
Query: {
...clinicalRegistrationResolver,
...clinicalSearchResultResolver,
...clinicalSubmissionResolver,
},
Mutation: {
...clearClinicalSubmissionResolver,
},
};

export default resolvers;
34 changes: 34 additions & 0 deletions src/schemas/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@

import get from 'lodash/get';
import {
ActiveClinicalSubmission,
SubmissionValidationError,
SubmissionValidationUpdate,
} from '../submission/submission-entities';
import { DeepReadonly } from 'deep-freeze';
import { getClinicalEntitiesData } from '../dictionary/api';

const ARRAY_DELIMITER_CHAR = '|';

Expand Down Expand Up @@ -209,11 +211,43 @@ const convertClinicalSubmissionUpdateToGql = (updateData: UpdateData) => {
};
};

const convertClinicalSubmissionDataToGql = (
programShortName: string,
data: {
submission: DeepReadonly<ActiveClinicalSubmission> | undefined;
batchErrors?: { message: string; batchNames: string[]; code: string }[];
},
) => {
const submission = get(data, 'submission', {} as Partial<typeof data.submission>);
const fileErrors = get(data, 'batchErrors', [] as typeof data.batchErrors);
const clinicalEntities = get(submission, 'clinicalEntities');
return {
id: submission?._id || undefined,
programShortName,
state: submission?.state || undefined,
version: submission?.version || undefined,
updatedBy: submission?.updatedBy || undefined,
updatedAt: submission?.updatedAt ? submission.updatedAt : undefined,
clinicalEntities: async () => {
const clinicalSubmissionTypeList = await getClinicalEntitiesData('false'); // to confirm for true or false
const filledClinicalEntities = clinicalSubmissionTypeList.map(clinicalType => ({
clinicalType,
...(clinicalEntities ? clinicalEntities[clinicalType.name] : {}),
}));
return filledClinicalEntities.map(clinicalEntity =>
convertClinicalSubmissionEntityToGql(clinicalEntity?.clinicalType.name, clinicalEntity),
);
},
fileErrors: fileErrors?.map(convertClinicalFileErrorToGql),
};
};

export {
convertClinicalRecordToGql,
convertRegistrationErrorToGql,
convertClinicalFileErrorToGql,
convertRegistrationStatsToGql,
RegistrationErrorData,
convertClinicalSubmissionEntityToGql,
convertClinicalSubmissionDataToGql,
};
39 changes: 36 additions & 3 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
* IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import { token } from 'morgan';

console.time('boot time');
// Has to import config before any other import uses the configurations
import { AppConfig, RxNormDbConfig, KafkaConfigurations } from './config';
Expand All @@ -29,11 +31,16 @@ import { Server } from 'http';
// we import here to allow configs to fully load
import * as bootstrap from './bootstrap';
import app from './app';
import { GlobalGqlContext } from './app';
import { database, up } from 'migrate-mongo';

import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { ApolloServer, ContextFunction } from '@apollo/server';
import {
StandaloneServerContextFunctionArgument,
startStandaloneServer,
} from '@apollo/server/standalone';
import schema from './schemas/index';
// import { EgoJwtData } from '@icgc-argo/ego-token-utils/dist/common';
UmmulkiramR marked this conversation as resolved.
Show resolved Hide resolved

let secrets: any = {};
let server: Server;
Expand Down Expand Up @@ -153,10 +160,36 @@ let server: Server;
/**
* Start Graphql server.
*/
const apolloServer = new ApolloServer({

const context: ContextFunction<
[StandaloneServerContextFunctionArgument],
GlobalGqlContext
> = async ({ req, res }) => {
// Get the user token from the headers.
const authHeader = req.headers.authorization;
let userJwtData = undefined;
try {
if (authHeader) {
const jwt = authHeader.replace('Bearer ', '');
}
} catch (err) {
userJwtData = undefined;
}
// Add the user to the context
return {
isUserRequest: true,
egoToken: (authHeader || '').split('Bearer ').join(''),
Authorization: `Bearer ${(authHeader || '').replace(/^Bearer[\s]*!/, '')}` || '',
dataLoaders: {},
};
};

const apolloServer = new ApolloServer<GlobalGqlContext>({
schema,
});

const { url } = await startStandaloneServer(apolloServer, {
context,
listen: { port: app.get('graphqlPort') },
});

Expand Down
36 changes: 29 additions & 7 deletions src/submission/submission-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { HasFullWriteAccess, HasProgramWriteAccess } from '../decorators';
import _ from 'lodash';
import { batchErrorMessage } from './submission-error-messages';
import * as fs from 'fs';
import { GlobalGqlContext } from '../app';

const L = loggerFor(__filename);
const fsPromises = fs.promises;
Expand Down Expand Up @@ -62,7 +63,7 @@ class SubmissionController {
return;
}
const programId = req.params.programId;
const creator = ControllerUtils.getUserFromToken(req);
const creator = ControllerUtils.getUserFromRequest(req);
const file = req.file;
let records: ReadonlyArray<TsvUtils.TsvRecordAsJsonObj>;
try {
Expand Down Expand Up @@ -136,7 +137,7 @@ class SubmissionController {
return;
}

const user = ControllerUtils.getUserFromToken(req);
const user = ControllerUtils.getUserFromRequest(req);
const newClinicalData: NewClinicalEntity[] = [];
const tsvParseErrors: SubmissionBatchError[] = [];
const clinicalFiles = req.files as Express.Multer.File[];
Expand Down Expand Up @@ -191,7 +192,7 @@ class SubmissionController {
async validateActiveSubmission(req: Request, res: Response) {
if (await submissionSystemIsDisabled(res)) return;
const { versionId, programId } = req.params;
const updater = ControllerUtils.getUserFromToken(req);
const updater = ControllerUtils.getUserFromRequest(req);
const result = await submission.operations.validateMultipleClinical({
versionId,
programId,
Expand All @@ -207,7 +208,7 @@ class SubmissionController {
async clearFileFromActiveSubmission(req: Request, res: Response) {
if (await submissionSystemIsDisabled(res)) return;
const { programId, versionId, fileType } = req.params;
const updater = ControllerUtils.getUserFromToken(req);
const updater = ControllerUtils.getUserFromRequest(req);
L.debug(`Entering clearFileFromActiveSubmission: ${{ programId, versionId, fileType }}`);
const updatedSubmission = await submission.operations.clearSubmissionData({
programId,
Expand All @@ -219,11 +220,32 @@ class SubmissionController {
return res.status(200).send(updatedSubmission || {});
}

// @HasProgramWriteAccess((programId: string) => programId)
async clearFileDataFromActiveSubmission(
programId: string,
fileType: string,
versionId: string,
token: string,
) {
const submissionSystemDisabled = await persistedConfig.getSubmissionDisabledState();
if (submissionSystemDisabled) return;
const updater = ControllerUtils.getUserFromToken(token);
L.debug(`Entering clearFileDataFromActiveSubmission: ${{ programId, versionId, fileType }}`);
const updatedSubmission = await submission.operations.clearSubmissionData({
programId,
versionId,
fileType,
updater,
});
// Handle case where submission was cleared and is now undefined
return updatedSubmission;
}

@HasProgramWriteAccess((req: Request) => req.params.programId)
async commitActiveSubmission(req: Request, res: Response) {
if (await submissionSystemIsDisabled(res)) return;
const { versionId, programId } = req.params;
const updater = ControllerUtils.getUserFromToken(req);
const updater = ControllerUtils.getUserFromRequest(req);
const activeSubmission = await submission2Clinical.commitClinicalSubmission({
versionId,
programId,
Expand All @@ -247,7 +269,7 @@ class SubmissionController {
async reopenActiveSubmission(req: Request, res: Response) {
if (await submissionSystemIsDisabled(res)) return;
const { versionId, programId } = req.params;
const updater = ControllerUtils.getUserFromToken(req);
const updater = ControllerUtils.getUserFromRequest(req);
const activeSubmission = await submission.operations.reopenClinicalSubmission({
versionId,
programId,
Expand Down Expand Up @@ -299,7 +321,7 @@ class SubmissionController {
const samplesSubmitterIds =
req.query.sampleSubmitterIds && req.query.sampleSubmitterIds.split(',');
const dryRun = req.query.dryRun === 'false' ? false : true;
const updater = ControllerUtils.getUserFromToken(req);
const updater = ControllerUtils.getUserFromRequest(req);
L.info(
`Delete registered samples called, caller ${updater}, ids: ${samplesSubmitterIds}, programId: ${programId}`,
);
Expand Down
Loading