Skip to content

Commit

Permalink
SIMSBIOHUB-632: Display Markdown in Vote Dialogs (#1418)
Browse files Browse the repository at this point in the history
* Add markdown database table.
* Add help buttons and ability for users to vote them up or down.

---------

Co-authored-by: Nick Phura <[email protected]>
  • Loading branch information
mauberti-bc and NickPhura authored Nov 25, 2024
1 parent 8f48b31 commit 0a9a57f
Show file tree
Hide file tree
Showing 41 changed files with 4,602 additions and 2,186 deletions.
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({
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({
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 {
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

0 comments on commit 0a9a57f

Please sign in to comment.