diff --git a/.github/workflows/scan.yaml b/.github/workflows/scan.yaml index fc9f6ed..f5752fe 100644 --- a/.github/workflows/scan.yaml +++ b/.github/workflows/scan.yaml @@ -1,7 +1,7 @@ name: build on: pull_request: - + jobs: build: name: Build diff --git a/.trivyignore b/.trivyignore new file mode 100644 index 0000000..1b82280 --- /dev/null +++ b/.trivyignore @@ -0,0 +1,2 @@ +# node-pkg, not fixed as of 2024-11-19 +CVE-2024-21538 \ No newline at end of file diff --git a/src/db/dal/variant.ts b/src/db/dal/variant.ts index 1192dd0..4374cda 100644 --- a/src/db/dal/variant.ts +++ b/src/db/dal/variant.ts @@ -1,35 +1,55 @@ import { Op } from 'sequelize'; + import VariantModel from '../models/Variant'; -export const addNewEntry = async function (uniqueId: string, organizationId: string, authorId: string, properties: any) { +export const addNewEntry = async function ( + uniqueId: string, + organizationId: string, + authorId: string, + properties: any, +) { + const variantFound = await VariantModel.findOne({ + order: [['timestamp', 'DESC']], + where: { + unique_id: { + [Op.eq]: uniqueId, + }, + author_id: { + [Op.eq]: authorId, + }, + }, + }); + + const combinedProperties = { ...(variantFound?.properties || {}), ...properties }; + return await VariantModel.create({ unique_id: uniqueId, organization_id: organizationId, author_id: authorId, - properties, - timestamp: new Date() + properties: combinedProperties, + timestamp: new Date(), }); -} +}; export const getEntriesByUniqueIdsAndOrganizations = async function (uniqueIds: string[], organizationIds: string[]) { return await VariantModel.findAll({ order: [['timestamp', 'DESC']], where: { unique_id: { - [Op.in]: uniqueIds + [Op.in]: uniqueIds, }, organization_id: { - [Op.in]: organizationIds - } - } - }); -} - -export const getEntriesByPropertiesFlags = async function (flags: string[], organizationIds: string[], uniqueIdParam: string) { - const flagsWhere = flags.map(f => { - return `properties @> '{"flags": ["${f}"]}'`; + [Op.in]: organizationIds, + }, + }, }); +}; +const getEntriesByProperties = async function ( + whereClause: string, + organizationIds: string[], + uniqueIdParam: string +) { const uniqueIdWhere = uniqueIdParam.length > 0 ? ` AND unique_id LIKE '%${uniqueIdParam}%'` : ''; const result = await VariantModel.sequelize.query(` @@ -43,7 +63,27 @@ export const getEntriesByPropertiesFlags = async function (flags: string[], orga FROM variants WHERE organization_id IN ('${organizationIds.join("', '")}') ${uniqueIdWhere} ) s - WHERE rnk = 1 AND (${flagsWhere.join(' OR ')});`); + WHERE rnk = 1 AND (${whereClause});`); return result; -} \ No newline at end of file +} + +export const getEntriesByPropertiesFlags = async function ( + flags: string[], + organizationIds: string[], + uniqueIdParam: string +) { + const flagsWhere = flags.map((f) => `properties @> '{"flags": ["${f}"]}'`); + + return await getEntriesByProperties(flagsWhere.join(' OR '), organizationIds, uniqueIdParam); +}; + +export const getEntriesByPropertiesNote = async function ( + hasNote: boolean, + organizationIds: string[], + uniqueIdParam: string +) { + const notesWhere = `properties ->> 'note' IS ${hasNote ? 'NOT NULL' : 'NULL'}`; + + return await getEntriesByProperties(notesWhere, organizationIds, uniqueIdParam); +} diff --git a/src/db/models/Variant.ts b/src/db/models/Variant.ts index 315bf6e..013c864 100644 --- a/src/db/models/Variant.ts +++ b/src/db/models/Variant.ts @@ -1,4 +1,5 @@ -import { CreationOptional, DataTypes, Model, Optional } from 'sequelize'; +import { CreationOptional, DataTypes, Model, Optional } from 'sequelize'; + import sequelizeConnection from '../config'; interface IVariantAttributes { @@ -7,7 +8,7 @@ interface IVariantAttributes { author_id: string; organization_id: string; timestamp: Date; - properties: Object; + properties: object; } type VariantCreationAttributes = Optional; @@ -50,19 +51,19 @@ VariantModel.init( defaultValue: new Date(), validate: { isDate: true, - } + }, }, properties: { type: DataTypes.JSONB, allowNull: false, defaultValue: {}, - } + }, }, { sequelize: sequelizeConnection, tableName: 'variants', - timestamps: false - } + timestamps: false, + }, ); -export default VariantModel; \ No newline at end of file +export default VariantModel; diff --git a/src/routes/variant.ts b/src/routes/variant.ts index 13040d9..cac6875 100644 --- a/src/routes/variant.ts +++ b/src/routes/variant.ts @@ -1,18 +1,18 @@ import { Request, Router } from 'express'; import { StatusCodes } from 'http-status-codes'; -import Realm from '../config/realm'; import { keycloakRealm } from '../config/env'; -import { addNewEntry, getEntriesByUniqueIdsAndOrganizations, getEntriesByPropertiesFlags } from '../db/dal/variant'; +import Realm from '../config/realm'; +import { addNewEntry, getEntriesByPropertiesFlags, getEntriesByPropertiesNote, getEntriesByUniqueIdsAndOrganizations } from '../db/dal/variant'; const CLIN_GENETICIAN_ROLE = 'clin_genetician'; -const EMPTY_PROPERTIES = { }; +const EMPTY_PROPERTIES = {}; interface UserInfo { - authorId: string, - userRoles: string[], - userOrganizations: string[] + authorId: string; + userRoles: string[]; + userOrganizations: string[]; } function getUserInfo(request: Request): UserInfo { @@ -23,7 +23,10 @@ function getUserInfo(request: Request): UserInfo { } function validateCreate(userInfo: UserInfo, organization_id: string) { - return userInfo.userRoles.indexOf(CLIN_GENETICIAN_ROLE) > -1 && userInfo.userOrganizations.indexOf(organization_id) > -1; + return ( + userInfo.userRoles.indexOf(CLIN_GENETICIAN_ROLE) > -1 && + userInfo.userOrganizations.indexOf(organization_id) > -1 + ); } function validateGet(userInfo: UserInfo) { @@ -42,7 +45,12 @@ variantRouter.post('/:unique_id/:organization_id', async (req, res, next) => { const canCreate = validateCreate(userInfo, req?.params?.organization_id); if (canCreate) { - const dbResponse = await addNewEntry(req?.params?.unique_id, req?.params?.organization_id, userInfo.authorId, req?.body || EMPTY_PROPERTIES); + const dbResponse = await addNewEntry( + req?.params?.unique_id, + req?.params?.organization_id, + userInfo.authorId, + req?.body || EMPTY_PROPERTIES, + ); return res.status(StatusCodes.CREATED).send(dbResponse); } else { return res.sendStatus(StatusCodes.FORBIDDEN); @@ -62,7 +70,7 @@ variantRouter.get('/', async (req, res, next) => { const canGet = validateGet(userInfo); let dbResponse = []; - let uniqueIds = []; + const uniqueIds = []; if (Array.isArray(req.query?.unique_id)) { uniqueIds.push(...req.query.unique_id); @@ -78,7 +86,6 @@ variantRouter.get('/', async (req, res, next) => { } else { return res.sendStatus(StatusCodes.BAD_REQUEST); } - } catch (e) { next(e); } @@ -94,7 +101,8 @@ variantRouter.get('/filter', async (req, res, next) => { const canGet = validateGet(userInfo); let dbResponse = []; - let flags = []; + const flags = []; + let hasNote = null; const flagsParam = req.query?.flag; if (Array.isArray(flagsParam)) { @@ -103,11 +111,23 @@ variantRouter.get('/filter', async (req, res, next) => { flags.push(flagsParam); } - let uniqueIdFilterParam = (req.query?.unique_id || '').toString().trim(); + const noteParams = req.query?.note; + if (typeof noteParams === 'string') { + if (noteParams === 'true') { + hasNote = true; + } else if (noteParams === 'false') { + hasNote = false; + } + } + + const uniqueIdFilterParam = (req.query?.unique_id || '').toString().trim(); - if (canGet && flags.length > 0) { + if (canGet && hasNote !== null) { + dbResponse = await getEntriesByPropertiesNote(hasNote, userInfo.userOrganizations, uniqueIdFilterParam); + return res.status(StatusCodes.OK).send(dbResponse[0].map((r) => r.unique_id)); + } else if (canGet && flags.length > 0) { dbResponse = await getEntriesByPropertiesFlags(flags, userInfo.userOrganizations, uniqueIdFilterParam); - return res.status(StatusCodes.OK).send(dbResponse[0].map(r => r.unique_id)); + return res.status(StatusCodes.OK).send(dbResponse[0].map((r) => r.unique_id)); } else if (!canGet) { return res.sendStatus(StatusCodes.FORBIDDEN); } else { @@ -118,4 +138,4 @@ variantRouter.get('/filter', async (req, res, next) => { } }); -export default variantRouter; \ No newline at end of file +export default variantRouter;