From fabeeff65c992793bebba0bdc604d605620e7d7c Mon Sep 17 00:00:00 2001 From: Daniel <79996555+daniel-cy-lu@users.noreply.github.com> Date: Mon, 6 Nov 2023 15:35:36 -0500 Subject: [PATCH 1/8] completed check list of this ticket, adding data center gql type, query and resolver (#700) --- src/schemas/Program/index.js | 30 +++++++++++++++++++++++ src/services/programService/httpClient.js | 15 ++++++++++-- src/services/programService/index.js | 2 ++ 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/schemas/Program/index.js b/src/schemas/Program/index.js index 60d589f3..5eeb6fbe 100644 --- a/src/schemas/Program/index.js +++ b/src/schemas/Program/index.js @@ -96,6 +96,22 @@ const typeDefs = gql` countries: [String]! } + type DataCenter { + id: ID + shortName: String! + name: String + organization: String + email: String + uiUrl: String + gatewayUrl: String + analysisSongCode: String + analysisSongUrl: String + analysisScoreUrl: String + submissionSongCode: String + submissionSongUrl: String + submissionScoreUrl: String + } + input ProgramUserInput { email: String! firstName: String! @@ -169,6 +185,11 @@ const typeDefs = gql` joinProgramInvite(id: ID!): JoinProgramInvite programOptions: ProgramOptions! + + """ + retrieve all DataCenters + """ + dataCenters: [DataCenter] } type Mutation { @@ -271,6 +292,11 @@ const resolveHTTPProgram = async (programShortName) => { return response ? formatHttpProgram(response) : null; }; +const resolveDataCenterList = async (egoToken) => { + const response = await programService.listDataCenters(egoToken); + return response || null; +}; + const programServicePrivateFields = [ 'commitmentDonors', 'submittedDonors', @@ -354,6 +380,10 @@ const resolvers = { return response ? grpcToGql(joinProgramDetails) : null; }, programOptions: () => ({}), + dataCenters: async (obj, args, context, info) => { + const { egoToken } = context; + return resolveDataCenterList(egoToken); + }, }, Mutation: { createProgram: async (obj, args, context, info) => { diff --git a/src/services/programService/httpClient.js b/src/services/programService/httpClient.js index 61ab3d4d..4b15e2ea 100644 --- a/src/services/programService/httpClient.js +++ b/src/services/programService/httpClient.js @@ -27,7 +27,7 @@ import fetch from 'node-fetch'; import { PROGRAM_SERVICE_HTTP_ROOT } from '../../config'; import { restErrorResponseHandler } from '../../utils/restUtils'; -const getProgramPublicFields = async (programShortName) => { +export const getProgramPublicFields = async (programShortName) => { const url = `${PROGRAM_SERVICE_HTTP_ROOT}/public/program?name=${programShortName}`; const response = await fetch(url, { method: 'get', @@ -37,4 +37,15 @@ const getProgramPublicFields = async (programShortName) => { return response; }; -export { getProgramPublicFields }; +export const listDataCenters = async (jwt) => { + const url = `${PROGRAM_SERVICE_HTTP_ROOT}/datacenters`; + const response = await fetch(url, { + method: 'get', + headers: { + Authorization: `Bearer ${jwt}`, + }, + }) + .then(restErrorResponseHandler) + .then((response) => response.json()); + return response; +}; diff --git a/src/services/programService/index.js b/src/services/programService/index.js index fe56d105..8c0fdfa9 100644 --- a/src/services/programService/index.js +++ b/src/services/programService/index.js @@ -46,4 +46,6 @@ export default { removeUser: grpc.removeUser, getProgramPublicFields: http.getProgramPublicFields, + + listDataCenters: http.listDataCenters, }; From 68918c9313a06956772511b8c2a6c63928579c9a Mon Sep 17 00:00:00 2001 From: Jon Eubank Date: Tue, 7 Nov 2023 14:14:31 -0500 Subject: [PATCH 2/8] Replace config type assertions with defaults declaring missing variable (#682) --- src/config.ts | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/config.ts b/src/config.ts index e64d60f5..77c447f2 100644 --- a/src/config.ts +++ b/src/config.ts @@ -23,8 +23,9 @@ export const ADVERTISED_HOST = process.env.ADVERTISED_HOST || 'http://localhost: // Elasticsearch config export const ELASTICSEARCH_HOST = process.env.ELASTICSEARCH_HOST || 'http://localhost:9200'; -export const ELASTICSEARCH_VAULT_SECRET_PATH = process.env - .ELASTICSEARCH_VAULT_SECRET_PATH as string; +export const ELASTICSEARCH_VAULT_SECRET_PATH = + process.env.ELASTICSEARCH_VAULT_SECRET_PATH || + 'Missing env variable ELASTICSEARCH_VAULT_SECRET_PATH'; export const ELASTICSEARCH_USERNAME = process.env.ELASTICSEARCH_USERNAME; export const ELASTICSEARCH_PASSWORD = process.env.ELASTICSEARCH_PASSWORD; export const ELASTICSEARCH_CLIENT_TRUST_SSL_CERT = @@ -44,28 +45,27 @@ export const ARRANGER_PROJECT_ID = process.env.ARRANGER_PROJECT_ID || 'argo'; export const EGO_ROOT_REST = process.env.EGO_ROOT_REST || 'http://localhost:8081'; export const EGO_ROOT_GRPC = process.env.EGO_ROOT_GRPC || 'localhost:50051'; export const EGO_DACO_POLICY_NAME = process.env.EGO_DACO_POLICY_NAME || 'DACO'; -export const EGO_VAULT_SECRET_PATH = process.env.EGO_VAULT_SECRET_PATH as string; +export const EGO_VAULT_SECRET_PATH = + process.env.EGO_VAULT_SECRET_PATH || 'Missing env variable EGO_VAULT_SECRET_PATH'; export const EGO_CLIENT_ID = process.env.EGO_CLIENT_ID; export const EGO_CLIENT_SECRET = process.env.EGO_CLIENT_SECRET; // Ego Credentials for Score Proxy -export const EGO_VAULT_SCORE_PROXY_SECRET_PATH = process.env - .EGO_VAULT_SCORE_PROXY_SECRET_PATH as string; +export const EGO_VAULT_SCORE_PROXY_SECRET_PATH = process.env.EGO_VAULT_SCORE_PROXY_SECRET_PATH; export const EGO_SCORE_PROXY_CLIENT_ID = process.env.EGO_SCORE_PROXY_CLIENT_ID; export const EGO_SCORE_PROXY_CLIENT_SECRET = process.env.EGO_SCORE_PROXY_CLIENT_SECRET; // Ego Credentials for Clinical API -export const EGO_VAULT_CLINICAL_API_SECRET_PATH = process.env - .EGO_VAULT_CLINICAL_API_SECRET_PATH as string; +export const EGO_VAULT_CLINICAL_API_SECRET_PATH = process.env.EGO_VAULT_CLINICAL_API_SECRET_PATH; export const EGO_CLINICAL_API_CLIENT_ID = process.env.EGO_CLINICAL_API_CLIENT_ID; export const EGO_CLINICAL_API_CLIENT_SECRET = process.env.EGO_CLINICAL_API_CLIENT_SECRET; // Vault export const USE_VAULT = process.env.USE_VAULT === 'true'; -export const VAULT_TOKEN = process.env.VAULT_TOKEN as string; -export const VAULT_AUTH_METHOD = process.env.VAULT_AUTH_METHOD as 'token' | 'kubernetes'; -export const VAULT_URL = (process.env.VAULT_URL as string) || 'http://localhost:8200'; -export const VAULT_ROLE = process.env.VAULT_ROLE as string; +export const VAULT_TOKEN = process.env.VAULT_TOKEN; +export const VAULT_AUTH_METHOD = process.env.VAULT_AUTH_METHOD; +export const VAULT_URL = process.env.VAULT_URL || 'http://localhost:8200'; +export const VAULT_ROLE = process.env.VAULT_ROLE; // Default ego public key value is the example value provided in the application.yml of the public overture repository export const EGO_PUBLIC_KEY = @@ -85,8 +85,9 @@ export const DATA_CENTER_REGISTRY_API_ROOT = export const APP_DIR = __dirname; // Helpdesk auth -export const JIRA_ADMIN_VAULT_CREDENTIALS_PATH = process.env - .JIRA_ADMIN_VAULT_CREDENTIALS_PATH as string; +export const JIRA_ADMIN_VAULT_CREDENTIALS_PATH = + process.env.JIRA_ADMIN_VAULT_CREDENTIALS_PATH || + 'Missing env variable JIRA_ADMIN_VAULT_CREDENTIALS_PATH'; export const JIRA_REST_URI = process.env.JIRA_REST_URI || 'https://extsd.oicr.on.ca/rest/servicedeskapi'; export const JIRA_SERVICEDESK_ID = process.env.JIRA_SERVICEDESK_ID || '9'; From 93ea65770aede7b8362148882d542fc6d0c55bb1 Mon Sep 17 00:00:00 2001 From: Jon Eubank Date: Tue, 7 Nov 2023 14:19:30 -0500 Subject: [PATCH 3/8] Remove Ego GRPC requests (#669) --- src/resources/Ego.proto | 59 ------------------------- src/schemas/User/index.ts | 47 ++------------------ src/services/ego/index.ts | 92 +-------------------------------------- 3 files changed, 5 insertions(+), 193 deletions(-) delete mode 100644 src/resources/Ego.proto diff --git a/src/resources/Ego.proto b/src/resources/Ego.proto deleted file mode 100644 index 8d082ee8..00000000 --- a/src/resources/Ego.proto +++ /dev/null @@ -1,59 +0,0 @@ -syntax = "proto3"; -import "google/protobuf/wrappers.proto"; - -option java_multiple_files = true; -option java_package = "bio.overture.ego.grpc"; -option java_outer_classname = "EgoProto"; - -package bio.overture.ego.grpc; - -service UserService { - rpc GetUser (GetUserRequest) returns (User) {} - rpc ListUsers (ListUsersRequest) returns (ListUsersResponse) {} -} - -message PagedRequest { - uint32 page_number = 1; - uint32 page_size = 2; - string order_by = 3; -} - -message PagedResponse { - uint32 max_results = 1; - google.protobuf.UInt32Value next_page = 2; -} - -message GetUserRequest { - string id = 1; -} - -message ListUsersRequest { - PagedRequest page = 1; - - google.protobuf.StringValue query = 2; - repeated string group_ids = 3; -} - -message ListUsersResponse { - PagedResponse page = 1; - - repeated User users = 2; -} - -message User { - google.protobuf.StringValue id = 1; - google.protobuf.StringValue email = 2; - google.protobuf.StringValue first_name = 3; - google.protobuf.StringValue last_name = 4; - - google.protobuf.StringValue created_at = 5; - google.protobuf.StringValue last_login = 6; - google.protobuf.StringValue name = 7; - google.protobuf.StringValue preferred_language = 8; - google.protobuf.StringValue status = 9; - google.protobuf.StringValue type = 10; - - repeated string applications = 11; - repeated string groups = 12; - repeated string scopes = 13; -} \ No newline at end of file diff --git a/src/schemas/User/index.ts b/src/schemas/User/index.ts index 515857bd..6f00d753 100644 --- a/src/schemas/User/index.ts +++ b/src/schemas/User/index.ts @@ -19,12 +19,11 @@ import { gql } from 'apollo-server-express'; import { makeExecutableSchema } from 'graphql-tools'; -import get from 'lodash/get'; -import { EgoClient, EgoGrpcUser, ListUserSortOptions } from '../../services/ego'; -import { EGO_DACO_POLICY_NAME } from '../../config'; -import egoTokenUtils from 'utils/egoTokenUtils'; import { GlobalGqlContext } from 'app'; +import egoTokenUtils from 'utils/egoTokenUtils'; +import { EGO_DACO_POLICY_NAME } from '../../config'; +import { EgoClient } from '../../services/ego'; import logger from '../../utils/logger'; // Construct a schema, using GraphQL schema language @@ -69,16 +68,6 @@ const typeDefs = gql` } type Query { - """ - retrieve User data by id - """ - user(id: String!): User - - """ - retrieve paginated list of user data - """ - users(pageNum: Int, limit: Int, sort: String, groups: [String], query: String): [User] - """ retrive user profile data """ @@ -93,22 +82,6 @@ const typeDefs = gql` } `; -const convertEgoUser = (user: EgoGrpcUser) => ({ - id: get(user, 'id.value'), - email: get(user, 'email.value'), - firstName: get(user, 'first_name.value'), - lastName: get(user, 'last_name.value'), - createdAt: get(user, 'created_at.value'), - lastLogin: get(user, 'last_login.value'), - name: get(user, 'name.value'), - preferredLanguage: get(user, 'preferred_language.value'), - status: get(user, 'status.value'), - type: get(user, 'type.value'), - applications: get(user, 'applications'), - groups: get(user, 'groups'), - scopes: get(user, 'scopes'), -}); - const createProfile = ({ apiKey, isDacoApproved, @@ -124,20 +97,6 @@ const createProfile = ({ const createResolvers = (egoClient: EgoClient) => { return { Query: { - user: async (obj: unknown, args: { id: string }, context: GlobalGqlContext) => { - const { egoToken } = context; - const egoUser: EgoGrpcUser = await egoClient.getUser(args.id, egoToken); - return egoUser === null ? null : convertEgoUser(egoUser); - }, - users: async (obj: unknown, args: ListUserSortOptions, context: GlobalGqlContext) => { - const { egoToken } = context; - const options = { - ...args, - }; - const response = await egoClient.listUsers(options, egoToken); - const egoUserList: EgoGrpcUser[] = get(response, 'users', []); - return egoUserList.map((egoUser) => convertEgoUser(egoUser)); - }, self: async (obj: unknown, args: undefined, context: GlobalGqlContext) => { const { Authorization, egoToken, userJwtData } = context; logger.info({ Authorization, egoToken, userJwtData }); diff --git a/src/services/ego/index.ts b/src/services/ego/index.ts index fbc6cf1f..fba8f31f 100644 --- a/src/services/ego/index.ts +++ b/src/services/ego/index.ts @@ -21,43 +21,14 @@ * This file dynamically generates a gRPC client from Ego.proto. * The content of Ego.proto is copied directly from: https://github.com/overture-stack/ego/blob/develop/src/main/proto/Ego.proto */ -import path from 'path'; - -import * as loader from '@grpc/proto-loader'; -import grpc, { ChannelCredentials } from 'grpc'; import memoize from 'lodash/memoize'; import fetch from 'node-fetch'; import urlJoin from 'url-join'; -import { APP_DIR, EGO_DACO_POLICY_NAME, EGO_ROOT_GRPC, EGO_ROOT_REST } from '../../config'; -import { defaultPromiseCallback, getAuthMeta, withRetries } from '../../utils/grpcUtils'; +import { EGO_DACO_POLICY_NAME, EGO_ROOT_REST } from '../../config'; import logger from '../../utils/logger'; import { restErrorResponseHandler } from '../../utils/restUtils'; -export type EgoGrpcUser = { - id: { value: unknown }; - email: { value: unknown }; - first_name: { value: unknown }; - last_name: { value: unknown }; - created_at: { value: unknown }; - last_login: { value: unknown }; - name: { value: unknown }; - preferred_language: { value: unknown }; - status: { value: unknown }; - type: { value: unknown }; - applications: unknown; - groups: unknown; - scopes: unknown; -}; - -export type ListUserSortOptions = { - pageNum?: number; - limit?: number; - sort?: string; - groups?: string[]; - query?: string; -}; - export type EgoApplicationCredential = { clientId: string; clientSecret: string; @@ -88,65 +59,12 @@ const createEgoClient = (applicationCredential: EgoApplicationCredential) => { `${applicationCredential.clientId}:${applicationCredential.clientSecret}`, ).toString('base64'); - const PROTO_PATH = path.join(APP_DIR, '/resources/Ego.proto'); - const EGO_API_KEY_ENDPOINT = urlJoin(EGO_ROOT_REST, '/o/api_key'); - const packageDefinition = loader.loadSync(PROTO_PATH, { - keepCase: true, - longs: String, - enums: String, - defaults: true, - oneofs: true, - }); - - const protoBio = grpc.loadPackageDefinition(packageDefinition).bio as { - overture: { - ego: { - grpc: { - UserService: new (grpc_root: string, credentials: ChannelCredentials) => any; - }; - }; - }; - }; - const proto = protoBio.overture.ego.grpc; - - const userService = withRetries( - new proto.UserService(EGO_ROOT_GRPC, grpc.credentials.createInsecure()), - ); let memoizedGetDacoIds: ReturnType | null = null; let dacoIdsCalled = new Date(); const dacoGroupIdExpiry = 86400000; // 24hours - const getUser = async (id: string, jwt: string | null = null): Promise => { - return await new Promise((resolve, reject) => { - userService.getUser( - { id }, - getAuthMeta(jwt), - defaultPromiseCallback(resolve, reject, 'Ego.getUser'), - ); - }); - }; - - const listUsers = async ( - { pageNum, limit, sort, groups, query }: ListUserSortOptions = {}, - jwt: string | null = null, - ) => { - const payload = { - page: { page_number: pageNum, page_size: limit, sort }, - group_ids: groups, - query: { value: query }, - }; - - return await new Promise((resolve, reject) => { - userService.listUsers( - payload, - getAuthMeta(jwt), - defaultPromiseCallback(resolve, reject, 'Ego.listUsers'), - ); - }); - }; - type EgoAccessKeyObj = { name: string; expiryDate: string; @@ -160,10 +78,6 @@ const createEgoClient = (applicationCredential: EgoApplicationCredential) => { userId: string, Authorization: string, ): Promise => { - type EgoApiKeyResponse = { - count: number; - resultSet: EgoAccessKeyObj[]; - }; const firstResponse = await fetch(urlJoin(EGO_API_KEY_ENDPOINT, `?user_id=${userId}`), { headers: { Authorization }, }) @@ -233,7 +147,7 @@ const createEgoClient = (applicationCredential: EgoApplicationCredential) => { method: 'delete', headers: { Authorization }, }) - .then((resp) => ({ key, success: true })) + .then((_) => ({ key, success: true })) .catch((err) => { logger.error(err); return { key, success: false }; @@ -326,8 +240,6 @@ const createEgoClient = (applicationCredential: EgoApplicationCredential) => { }; return { - getUser, - listUsers, generateEgoAccessKey, getScopes, getEgoAccessKeys, From 26182167db397187f06d76b871d50fdef6cf1296 Mon Sep 17 00:00:00 2001 From: Daniel <79996555+daniel-cy-lu@users.noreply.github.com> Date: Thu, 9 Nov 2023 16:35:14 -0500 Subject: [PATCH 4/8] completed ticket, add a function getDataCenterByShortName. add optional parameter to dataCenter query by shortName and update resolver to use this function and query (#701) --- src/schemas/Program/index.js | 11 ++++------- src/services/programService/httpClient.js | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/schemas/Program/index.js b/src/schemas/Program/index.js index 5eeb6fbe..f10f4b19 100644 --- a/src/schemas/Program/index.js +++ b/src/schemas/Program/index.js @@ -189,7 +189,7 @@ const typeDefs = gql` """ retrieve all DataCenters """ - dataCenters: [DataCenter] + dataCenters(shortName: String): [DataCenter] } type Mutation { @@ -292,11 +292,6 @@ const resolveHTTPProgram = async (programShortName) => { return response ? formatHttpProgram(response) : null; }; -const resolveDataCenterList = async (egoToken) => { - const response = await programService.listDataCenters(egoToken); - return response || null; -}; - const programServicePrivateFields = [ 'commitmentDonors', 'submittedDonors', @@ -382,7 +377,9 @@ const resolvers = { programOptions: () => ({}), dataCenters: async (obj, args, context, info) => { const { egoToken } = context; - return resolveDataCenterList(egoToken); + const shortName = get(args, 'shortName', null); + const response = await programService.listDataCenters(shortName, egoToken); + return response || null; }, }, Mutation: { diff --git a/src/services/programService/httpClient.js b/src/services/programService/httpClient.js index 4b15e2ea..2dde4927 100644 --- a/src/services/programService/httpClient.js +++ b/src/services/programService/httpClient.js @@ -27,6 +27,13 @@ import fetch from 'node-fetch'; import { PROGRAM_SERVICE_HTTP_ROOT } from '../../config'; import { restErrorResponseHandler } from '../../utils/restUtils'; +//get DataCenter by Program short name handler +const getDataCenterByShortName = (shortName, dataCenterResponse) => { + return dataCenterResponse.filter((dataCenterObject) => { + return dataCenterObject.shortName === shortName; + }); +}; + export const getProgramPublicFields = async (programShortName) => { const url = `${PROGRAM_SERVICE_HTTP_ROOT}/public/program?name=${programShortName}`; const response = await fetch(url, { @@ -37,7 +44,7 @@ export const getProgramPublicFields = async (programShortName) => { return response; }; -export const listDataCenters = async (jwt) => { +export const listDataCenters = async (shortName, jwt) => { const url = `${PROGRAM_SERVICE_HTTP_ROOT}/datacenters`; const response = await fetch(url, { method: 'get', @@ -46,6 +53,9 @@ export const listDataCenters = async (jwt) => { }, }) .then(restErrorResponseHandler) - .then((response) => response.json()); + .then((response) => response.json()) + .then((response) => { + return shortName ? getDataCenterByShortName(shortName, response) : response; + }); return response; }; From 8a113d5a99d081086f29507083016640aaca85d4 Mon Sep 17 00:00:00 2001 From: Daniel <79996555+daniel-cy-lu@users.noreply.github.com> Date: Mon, 13 Nov 2023 15:17:35 -0500 Subject: [PATCH 5/8] Feat/674 rest api integration (#677) * commit before branch change * replacing GRPC to HTTP method for public single program, private single program and private program list endpoints * remove comments * add joinProgramInvite http fetch and update resolver. some resolved error remain. git commit before switching branch * fix error from date values and waiting program service to fix first and last name * remove unused key value pairs in joinProgramInvite response * updated name * edit http fetch to match new Program Service api data format * remove testing console.log * implement urljoin * add util.js file to services/programService folder and add authorizationHeader to handle repetive part in fetch header * moved authorizationHeader to a ts file in a new folder, utils * implement logger and updated error message to be specific and detail --------- Co-authored-by: Dan --- src/schemas/Program/index.js | 47 ++---- src/services/programService/httpClient.js | 149 ++++++++++++++++-- src/services/programService/index.js | 9 +- .../utils/authorizationHeader.ts | 28 ++++ 4 files changed, 181 insertions(+), 52 deletions(-) create mode 100644 src/services/programService/utils/authorizationHeader.ts diff --git a/src/schemas/Program/index.js b/src/schemas/Program/index.js index f10f4b19..2ad6e58c 100644 --- a/src/schemas/Program/index.js +++ b/src/schemas/Program/index.js @@ -233,7 +233,7 @@ const typeDefs = gql` `; /* ========= - Convert GRPC Response to GQL output +HTTP resolvers * ========= */ const getIsoDate = (time) => (time ? new Date(parseInt(time) * 1000).toISOString() : null); @@ -263,33 +263,19 @@ const convertGrpcUserToGql = (userDetails) => ({ inviteAcceptedAt: getIsoDate(get(userDetails, 'accepted_at.seconds')), }); -const formatHttpProgram = (program) => ({ - name: program.name, - shortName: program.shortName, - description: program.description, - website: program.website, - institutions: program.programInstitutions?.map((institution) => institution.name) || [], - countries: program.programCountries?.map((country) => country.name) || [], - regions: program.processingRegions?.map((region) => region.name) || [], - cancerTypes: program.programCancers?.map((cancer) => cancer.name) || [], - primarySites: program.programPrimarySites?.map((primarySite) => primarySite.name) || [], -}); - -const resolveProgramList = async (egoToken) => { - const response = await programService.listPrograms(egoToken); - const programs = get(response, 'programs', []); - return programs.map((program) => convertGrpcProgramToGql(program)); +const resolvePrivateProgramList = async (egoToken) => { + const response = await programService.listPrivatePrograms(egoToken); + return response || null; }; -const resolveSingleProgram = async (egoToken, programShortName) => { - const response = await programService.getProgram(programShortName, egoToken); - const programDetails = get(response, 'program'); - return response ? convertGrpcProgramToGql(programDetails) : null; +const resolvePrivateSingleProgram = async (egoToken, programShortName) => { + const response = await programService.getPrivateProgram(egoToken, programShortName); + return response || null; }; -const resolveHTTPProgram = async (programShortName) => { - const response = await programService.getProgramPublicFields(programShortName); - return response ? formatHttpProgram(response) : null; +const resolvePublicSingleProgram = async (programShortName) => { + const response = await programService.getPublicProgram(programShortName); + return response || null; }; const programServicePrivateFields = [ @@ -360,19 +346,18 @@ const resolvers = { ); return hasPrivateField - ? resolveSingleProgram(egoToken, shortName) - : resolveHTTPProgram(shortName); + ? resolvePrivateSingleProgram(egoToken, shortName) + : resolvePublicSingleProgram(shortName); }, programs: async (obj, args, context, info) => { const { egoToken } = context; - return resolveProgramList(egoToken); + return resolvePrivateProgramList(egoToken); }, joinProgramInvite: async (obj, args, context, info) => { const { egoToken } = context; - const response = await programService.getJoinProgramInvite(args.id, egoToken); - const joinProgramDetails = get(response, 'invitation'); - return response ? grpcToGql(joinProgramDetails) : null; + const response = await programService.getJoinProgramInvite(egoToken, args.id); + return response || null; }, programOptions: () => ({}), dataCenters: async (obj, args, context, info) => { @@ -395,7 +380,7 @@ const resolvers = { try { const createResponse = await programService.createProgram(program, egoToken); - return resolveSingleProgram(egoToken, program.shortName); + return resolvePrivateSingleProgram(egoToken, program.shortName); } catch (err) { const GRPC_INVALID_ARGUMENT_ERROR_CODE = 3; if (err.code === GRPC_INVALID_ARGUMENT_ERROR_CODE) { diff --git a/src/services/programService/httpClient.js b/src/services/programService/httpClient.js index 2dde4927..27d6167d 100644 --- a/src/services/programService/httpClient.js +++ b/src/services/programService/httpClient.js @@ -23,39 +23,156 @@ */ import fetch from 'node-fetch'; +import urljoin from 'url-join'; import { PROGRAM_SERVICE_HTTP_ROOT } from '../../config'; import { restErrorResponseHandler } from '../../utils/restUtils'; -//get DataCenter by Program short name handler -const getDataCenterByShortName = (shortName, dataCenterResponse) => { - return dataCenterResponse.filter((dataCenterObject) => { - return dataCenterObject.shortName === shortName; - }); +import authorizationHeader from './utils/authorizationHeader'; + +import logger from 'utils/logger'; + +//data formatters +const formatPublicProgram = (program) => ({ + name: program.name, + shortName: program.shortName, + description: program.description, + website: program.website, + institutions: program.programInstitutions?.map((institution) => institution.name) || [], + countries: program.programCountries?.map((country) => country.name) || [], + regions: program.processingRegions?.map((region) => region.name) || [], + cancerTypes: program.programCancers?.map((cancer) => cancer.name) || [], + primarySites: program.programPrimarySites?.map((primarySite) => primarySite.name) || [], +}); + +const formatPrivateProgram = (program) => program.program; +const formatPrivateProgramList = (programList) => programList.map(formatPrivateProgram); + +const formatJoinProgramInvite = (invitation) => { + const formattedObj = { + ...invitation, + createdAt: new Date(invitation.createdAt), + expiresAt: new Date(invitation.expiresAt), + acceptedAt: new Date(invitation.acceptedAt), + user: { ...invitation.user, role: invitation.user.role.value }, + program: { + ...invitation.program, + institutions: invitation.program.programInstitutions, + countries: invitation.program.programCountries, + cancerTypes: invitation.program.programCancers, + primarySite: invitation.program.programPrimarySites, + }, + }; + + delete formattedObj.program.programInstitutions; + delete formattedObj.program.programCountries; + delete formattedObj.program.programCancers; + delete formattedObj.program.programPrimarySites; + return formattedObj; +}; + +const getDataCenterByShortName = (shortName, dataCenterResponse) => + dataCenterResponse.filter((dataCenterObject) => dataCenterObject.shortName === shortName); +//private fields +export const listPrivatePrograms = async (jwt = null) => { + const url = urljoin(PROGRAM_SERVICE_HTTP_ROOT, `/programs`); + return await fetch(url, { + method: 'get', + headers: { + Authorization: authorizationHeader(jwt), + }, + }) + .then(restErrorResponseHandler) + .then((response) => response.json()) + .then((data) => { + if (data && Array.isArray(data)) { + return formatPrivateProgramList(data); + } else { + logger.error( + 'Error: no data or wrong data type is returned from /programs. Data must be an array', + ); + throw new Error( + 'no data or wrong data type is returned from /programs. Data must be an array', + ); + } + }); +}; + +export const getPrivateProgram = async (jwt = null, programShortName) => { + const url = urljoin(PROGRAM_SERVICE_HTTP_ROOT, `/programs/${programShortName}`); + return await fetch(url, { + method: 'get', + headers: { + Authorization: authorizationHeader(jwt), + }, + }) + .then(restErrorResponseHandler) + .then((response) => response.json()) + .then((data) => { + if (data) { + return formatPrivateProgram(data); + } else { + logger.error('Error: no data is returned from /program/{shortName}'); + throw new Error('No data is returned from /program/{shortName}'); + } + }); }; -export const getProgramPublicFields = async (programShortName) => { - const url = `${PROGRAM_SERVICE_HTTP_ROOT}/public/program?name=${programShortName}`; - const response = await fetch(url, { +export const getJoinProgramInvite = async (jwt = null, id) => { + const url = urljoin(PROGRAM_SERVICE_HTTP_ROOT, `/programs/joinProgramInvite/${id}`); + return await fetch(url, { method: 'get', + headers: { + Authorization: authorizationHeader(jwt), + }, }) .then(restErrorResponseHandler) - .then((response) => response.json()); - return response; + .then((response) => response.json()) + .then((data) => { + if (data.invitation) { + return formatJoinProgramInvite(data.invitation); + } else { + logger.error( + 'Error: no data or wrong data type is returned from /programs/joinProgramInvite/{invite_id}. Data must be an object with a property of "invitation"', + ); + throw new Error( + 'No data or wrong data type is returned from /programs/joinProgramInvite/{invite_id}. Data must be an object with a property of "invitation"', + ); + } + }); }; export const listDataCenters = async (shortName, jwt) => { - const url = `${PROGRAM_SERVICE_HTTP_ROOT}/datacenters`; - const response = await fetch(url, { + const url = urljoin(PROGRAM_SERVICE_HTTP_ROOT, `/datacenters`); + return await fetch(url, { method: 'get', headers: { - Authorization: `Bearer ${jwt}`, + Authorization: authorizationHeader(jwt), }, }) .then(restErrorResponseHandler) .then((response) => response.json()) - .then((response) => { - return shortName ? getDataCenterByShortName(shortName, response) : response; + .then((data) => { + if (data && Array.isArray(data)) { + return shortName ? getDataCenterByShortName(shortName, data) : data; + } else { + logger.error( + 'Error: no data or wrong data type is returned from /datacenters. Data must be an array', + ); + throw new Error( + 'No data or wrong data type is returned from /datacenters. Data must be an array', + ); + } }); - return response; +}; + +// public fields +export const getPublicProgram = async (programShortName) => { + const url = urljoin(PROGRAM_SERVICE_HTTP_ROOT, `public/program?name=${programShortName}`); + return await fetch(url, { + method: 'get', + }) + .then(restErrorResponseHandler) + .then((response) => response.json()) + .then(formatPublicProgram); }; diff --git a/src/services/programService/index.js b/src/services/programService/index.js index 8c0fdfa9..3b240e90 100644 --- a/src/services/programService/index.js +++ b/src/services/programService/index.js @@ -26,9 +26,9 @@ import * as grpc from './grpcClient.js'; import * as http from './httpClient.js'; export default { - getProgram: grpc.getProgram, - listPrograms: grpc.listPrograms, - getJoinProgramInvite: grpc.getJoinProgramInvite, + getPrivateProgram: http.getPrivateProgram, + listPrivatePrograms: http.listPrivatePrograms, + getJoinProgramInvite: http.getJoinProgramInvite, listUsers: grpc.listUsers, listCancers: grpc.listCancers, @@ -45,7 +45,6 @@ export default { updateUser: grpc.updateUser, removeUser: grpc.removeUser, - getProgramPublicFields: http.getProgramPublicFields, - + getPublicProgram: http.getPublicProgram, listDataCenters: http.listDataCenters, }; diff --git a/src/services/programService/utils/authorizationHeader.ts b/src/services/programService/utils/authorizationHeader.ts new file mode 100644 index 00000000..8a92ddbd --- /dev/null +++ b/src/services/programService/utils/authorizationHeader.ts @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2022 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 . + * + * 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. + */ + +/* + * This file dynamically generates a gRPC client from Ego.proto. + * The content of Ego.proto is copied directly from: https://github.com/icgc-argo/argo-proto/blob/4e2aeda59eb48b7af20b462aef2f04ef5d0d6e7c/ProgramService.proto + */ + +// Create a function that return this repetitive string template in the fecth header +const authorizationHeader = (jwt: string) => `Bearer ${jwt}`; + +export default authorizationHeader; From deb1eab55a90ae8036526677f1fe7370a3e61968 Mon Sep 17 00:00:00 2001 From: Daniel <79996555+daniel-cy-lu@users.noreply.github.com> Date: Fri, 17 Nov 2023 15:44:54 -0500 Subject: [PATCH 6/8] add dataCenter to gql type Program, and add dataCenter to the list of private field. Test the resolver works with dataCenter (#705) --- src/schemas/Program/index.js | 2 ++ src/services/programService/httpClient.js | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/schemas/Program/index.js b/src/schemas/Program/index.js index 2ad6e58c..a7e4d538 100644 --- a/src/schemas/Program/index.js +++ b/src/schemas/Program/index.js @@ -72,6 +72,7 @@ const typeDefs = gql` regions: [String] cancerTypes: [String] primarySites: [String] + dataCenter: DataCenter membershipType: MembershipType @@ -284,6 +285,7 @@ const programServicePrivateFields = [ 'genomicDonors', 'membershipType', 'users', + 'dataCenter', ]; const resolvers = { diff --git a/src/services/programService/httpClient.js b/src/services/programService/httpClient.js index 27d6167d..2d22ef6f 100644 --- a/src/services/programService/httpClient.js +++ b/src/services/programService/httpClient.js @@ -25,13 +25,13 @@ import fetch from 'node-fetch'; import urljoin from 'url-join'; +import logger from 'utils/logger'; + import { PROGRAM_SERVICE_HTTP_ROOT } from '../../config'; import { restErrorResponseHandler } from '../../utils/restUtils'; import authorizationHeader from './utils/authorizationHeader'; -import logger from 'utils/logger'; - //data formatters const formatPublicProgram = (program) => ({ name: program.name, From 2a173b73d8c31b80a67e99ce05ea3e2c572d3515 Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 22 Nov 2023 10:55:48 -0500 Subject: [PATCH 7/8] =?UTF-8?q?=F0=9F=97=BA=EF=B8=8F=20=20662=20Filter=20P?= =?UTF-8?q?rograms=20by=20Region=20(#704)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Test Stubs * Filter for Countries * Use regions * Remove countries from gql definition * Updated DataCenter Filter * Replace region w/ datacenter * Capitalize Center * Unnecessary Empty Line * Change Query to Filter * Skip filtering when no DataCenter * Invert Conditional --- src/schemas/Clinical/index.ts | 2 -- src/schemas/Program/index.js | 16 ++++++++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/schemas/Clinical/index.ts b/src/schemas/Clinical/index.ts index 4862e1f1..0280ee47 100644 --- a/src/schemas/Clinical/index.ts +++ b/src/schemas/Clinical/index.ts @@ -469,8 +469,6 @@ const resolvers = { ) => { const { Authorization } = context; - console.log('clinical errors'); - const errorResponse: ClinicalErrors = await clinicalService.getClinicalErrors( args.programShortName, args.donorIds, diff --git a/src/schemas/Program/index.js b/src/schemas/Program/index.js index a7e4d538..7674bad6 100644 --- a/src/schemas/Program/index.js +++ b/src/schemas/Program/index.js @@ -23,7 +23,6 @@ import { get, merge, pickBy } from 'lodash'; import customScalars from 'schemas/customScalars'; import programService from 'services/programService'; -import { grpcToGql } from 'utils/grpcUtils'; const typeDefs = gql` scalar DateTime @@ -178,7 +177,7 @@ const typeDefs = gql` """ retrieve all Programs """ - programs: [Program] + programs(dataCenter: String): [Program] """ retrieve join program invitation by id @@ -352,10 +351,19 @@ const resolvers = { : resolvePublicSingleProgram(shortName); }, - programs: async (obj, args, context, info) => { + programs: async (obj, args, context) => { const { egoToken } = context; - return resolvePrivateProgramList(egoToken); + const { dataCenter } = args; + + const programs = await resolvePrivateProgramList(egoToken); + + const filteredPrograms = dataCenter + ? programs.filter((program) => program.dataCenter?.shortName === dataCenter) + : programs; + + return filteredPrograms; }, + joinProgramInvite: async (obj, args, context, info) => { const { egoToken } = context; const response = await programService.getJoinProgramInvite(egoToken, args.id); From ee75ea1cf60a5c7226c679bac1f54a85800b1553 Mon Sep 17 00:00:00 2001 From: Dan DeMaria Date: Tue, 5 Dec 2023 10:49:02 -0500 Subject: [PATCH 8/8] RC 3.43.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index bd38bd19..28ec0ff3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "server", - "version": "3.42.0", + "version": "3.43.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "server", - "version": "3.42.0", + "version": "3.43.0", "license": "ISC", "dependencies": { "@arranger/server": "^2.18.2", diff --git a/package.json b/package.json index c2e6c3df..4a456c19 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "server", - "version": "3.42.0", + "version": "3.43.0", "description": "", "main": "index.js", "scripts": {