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

SIMSBIOHUB-632: Display Markdown in Vote Dialogs #1418

Merged
merged 14 commits into from
Nov 25, 2024
3,680 changes: 1,632 additions & 2,048 deletions api/package-lock.json

Large diffs are not rendered by default.

36 changes: 36 additions & 0 deletions api/src/database-models/markdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { z } from 'zod';

/**
* Markdown Model.
*
* @description Data model for `markdown`.
*/
export const MarkdownModel = z.object({
markdown_id: z.number(),
markdown_type_id: z.number(),
data: z.string().nullable(),
score: z.number(),
record_end_date: z.string(),
create_date: z.string(),
create_user: z.number(),
update_date: z.string().nullable(),
update_user: z.number().nullable(),
revision_count: z.number()
});

export type MarkdownModel = z.infer<typeof MarkdownModel>;

/**
* Markdown Record.
*
* @description Data record for `markdown`.
*/
export const MarkdownRecord = MarkdownModel.omit({
create_date: true,
create_user: true,
update_date: true,
update_user: true,
revision_count: true
});

export type MarkdownRecord = z.infer<typeof MarkdownRecord>;
34 changes: 34 additions & 0 deletions api/src/database-models/markdown_type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { z } from 'zod';

/**
* Markdown Type Model.
*
* @description Data model for `markdown_type`.
*/
export const MarkdownTypeModel = z.object({

Check warning on line 8 in api/src/database-models/markdown_type.ts

View check run for this annotation

Codecov / codecov/patch

api/src/database-models/markdown_type.ts#L8

Added line #L8 was not covered by tests
markdown_type_id: z.number(),
name: z.string(),
description: z.string(),
create_date: z.string(),
create_user: z.number(),
update_date: z.string().nullable(),
update_user: z.number().nullable(),
revision_count: z.number()
});

export type MarkdownTypeModel = z.infer<typeof MarkdownTypeModel>;

/**
* Markdown Type Record.
*
* @description Data record for `markdown_type`.
*/
export const MarkdownTypeRecord = MarkdownTypeModel.omit({

Check warning on line 26 in api/src/database-models/markdown_type.ts

View check run for this annotation

Codecov / codecov/patch

api/src/database-models/markdown_type.ts#L26

Added line #L26 was not covered by tests
create_date: true,
create_user: true,
update_date: true,
update_user: true,
revision_count: true
});

export type MarkdownTypeRecord = z.infer<typeof MarkdownTypeRecord>;
34 changes: 34 additions & 0 deletions api/src/database-models/markdown_user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { z } from 'zod';

/**
* Markdown User Model.
*
* @description Data model for `markdown_user`.
*/
export const MarkdownUserModel = z.object({
markdown_user_id: z.number(),
system_user_id: z.number(),
markdown_id: z.number(),
create_date: z.string(),
create_user: z.number(),
update_date: z.string().nullable(),
update_user: z.number().nullable(),
revision_count: z.number()
});

export type MarkdownUserModel = z.infer<typeof MarkdownUserModel>;

/**
* Markdown User Record.
*
* @description Data record for `markdown_user`.
*/
export const MarkdownUserRecord = MarkdownUserModel.omit({
create_date: true,
create_user: true,
update_date: true,
update_user: true,
revision_count: true
});

export type MarkdownUserRecord = z.infer<typeof MarkdownUserRecord>;
17 changes: 17 additions & 0 deletions api/src/models/markdown-view.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { z } from 'zod';
import { MarkdownRecord } from '../database-models/markdown';

export const MarkdownObject = MarkdownRecord.pick({
markdown_id: true,
markdown_type_id: true,
data: true
}).extend({
participated: z.boolean()
});

export type MarkdownObject = z.infer<typeof MarkdownObject>;

export interface MarkdownQueryObject {
system_user_id: number;
markdown_type_name: string;
}
40 changes: 40 additions & 0 deletions api/src/openapi/schemas/markdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { OpenAPIV3 } from 'openapi-types';

/**
* Schema for markdown records used in versioned help dialogs
*
*/
export const markdownSchema: OpenAPIV3.SchemaObject = {
type: 'object',
description: 'Schema for get markdown response',
additionalProperties: false,
required: ['markdown'],
properties: {
markdown: {
type: 'object',
description: 'Markdown record',
required: ['markdown_id', 'markdown_type_id', 'data', 'participated'],
additionalProperties: false,
properties: {
markdown_id: {
type: 'number',
description: 'Primary key of the markdown record',
minimum: 1
},
markdown_type_id: {
type: 'number',
description: 'Type of the markdown record, used to identify which records correspond to which dialogs',
minimum: 1
},
data: {
type: 'string',
description: 'Markdown string to display'
},
participated: {
type: 'boolean',
description: 'True if the user has already scored the markdown record, otherwise false.'
}
}
}
}
};
93 changes: 93 additions & 0 deletions api/src/paths/markdown/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import chai, { expect } from 'chai';
import sinon from 'sinon';
import sinonChai from 'sinon-chai';
import { getMarkdownByTypeName } from '.';
import * as db from '../../database/db';
import { HTTPError } from '../../errors/http-error';
import { MarkdownService } from '../../services/markdown-service';
import { KeycloakUserInformation } from '../../utils/keycloak-utils';
import { getMockDBConnection, getRequestHandlerMocks } from '../../__mocks__/db';

chai.use(sinonChai);

describe('getMarkdown', () => {
afterEach(() => {
sinon.restore();
});

it('successfully retrieves markdown', async () => {
const mockMarkdownResponse = {
markdown_id: 1,
markdown_type_id: 1,
data: 'Sample markdown content',
participated: false
};

const mockDBConnection = getMockDBConnection({
open: sinon.stub(),
commit: sinon.stub(),
release: sinon.stub(),
systemUserId: () => 20
});

sinon.stub(db, 'getDBConnection').returns(mockDBConnection);

const getMarkdownStub = sinon
.stub(MarkdownService.prototype, 'getMarkdownByTypeName')
.resolves(mockMarkdownResponse);

const { mockReq, mockRes, mockNext } = getRequestHandlerMocks();
mockReq.query = { typeName: 'help' };
mockReq.keycloak_token = {} as KeycloakUserInformation;

const requestHandler = getMarkdownByTypeName();

await requestHandler(mockReq, mockRes, mockNext);

expect(mockDBConnection.open).to.have.been.calledOnce;
expect(mockDBConnection.commit).to.have.been.calledOnce;
expect(getMarkdownStub).to.have.been.calledOnceWith({
markdown_type_name: 'help',
system_user_id: 20
});
expect(mockRes.jsonValue.markdown).to.eql(mockMarkdownResponse);
expect(mockDBConnection.release).to.have.been.calledOnce;
});

it('handles errors gracefully', async () => {
const mockDBConnection = getMockDBConnection({
open: sinon.stub(),
commit: sinon.stub(),
rollback: sinon.stub(),
release: sinon.stub(),
systemUserId: () => 20
});

sinon.stub(db, 'getDBConnection').returns(mockDBConnection);

const getMarkdownStub = sinon
.stub(MarkdownService.prototype, 'getMarkdownByTypeName')
.rejects(new Error('a test error'));

const { mockReq, mockRes, mockNext } = getRequestHandlerMocks();
mockReq.query = { typeName: 'help' };
mockReq.keycloak_token = {} as KeycloakUserInformation;

const requestHandler = getMarkdownByTypeName();

try {
await requestHandler(mockReq, mockRes, mockNext);
expect.fail('Expected error was not thrown');
} catch (actualError) {
expect(mockDBConnection.open).to.have.been.calledOnce;
expect(getMarkdownStub).to.have.been.calledOnceWith({
markdown_type_name: 'help',
system_user_id: 20
});
expect(mockDBConnection.rollback).to.have.been.calledOnce;
expect(mockDBConnection.release).to.have.been.calledOnce;

expect((actualError as HTTPError).message).to.equal('a test error');
}
});
});
106 changes: 106 additions & 0 deletions api/src/paths/markdown/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { RequestHandler } from 'express';
import { Operation } from 'express-openapi';
import { getDBConnection } from '../../database/db';
import { markdownSchema } from '../../openapi/schemas/markdown';
import { authorizeRequestHandler } from '../../request-handlers/security/authorization';
import { MarkdownService } from '../../services/markdown-service';
import { getLogger } from '../../utils/logger';

const defaultLog = getLogger('paths/markdown/index');

export const GET: Operation = [
authorizeRequestHandler(() => {
return {

Check warning on line 13 in api/src/paths/markdown/index.ts

View check run for this annotation

Codecov / codecov/patch

api/src/paths/markdown/index.ts#L13

Added line #L13 was not covered by tests
and: [
{
discriminator: 'SystemUser'
}
]
};
}),
getMarkdownByTypeName()
];

GET.apiDoc = {
description: 'Gets a markdown record to display in a help dialog.',
tags: ['markdown'],
security: [
{
Bearer: []
}
],
parameters: [
{
in: 'query',
name: 'typeName',
description: 'The name of a markdown type to retrieve the latest markdown record for',
required: true,
schema: {
type: 'string'
}
}
],
responses: {
200: {
description: 'Markdown response object.',
content: {
'application/json': {
schema: markdownSchema
}
}
},
400: {
$ref: '#/components/responses/400'
},
401: {
$ref: '#/components/responses/401'
},
403: {
$ref: '#/components/responses/403'
},
500: {
$ref: '#/components/responses/500'
},
default: {
$ref: '#/components/responses/default'
}
}
};

/**
* Get the latest markdown text for a given markdown type
*
* @returns {RequestHandler}
*/
export function getMarkdownByTypeName(): RequestHandler {
return async (req, res) => {
defaultLog.debug({ label: 'getMarkdownByTypeName' });

const connection = getDBConnection(req.keycloak_token);

try {
await connection.open();

const systemUserId = connection.systemUserId();

const markdownTypeName = req.query.typeName as string;

const markdownService = new MarkdownService(connection);

const markdown = await markdownService.getMarkdownByTypeName({
markdown_type_name: markdownTypeName,
system_user_id: systemUserId
});

await connection.commit();

return res.status(200).json({ markdown });
} catch (error) {
defaultLog.error({ label: 'getMarkdown', message: 'error', error });
await connection.rollback();
throw error;
} finally {
connection.release();
}
};
}
Loading
Loading