diff --git a/src/schema/index.ts b/src/schema/index.ts index 97fefcd..e61a71f 100644 --- a/src/schema/index.ts +++ b/src/schema/index.ts @@ -26,6 +26,7 @@ import streetfindEventResolvers from './resolvers/eventStreetfind'; import giveawayEventResolvers from './resolvers/eventGiveaway'; import favoriteAnimalResolvers from './resolvers/favoriteAnimal'; import customScalarsResolvers from './resolvers/scalars'; +import medicationEventResolvers from './resolvers/eventMedication'; const schema = loadSchemaSync('src/schema/typeDefs/*.graphql', { loaders: [new GraphQLFileLoader()], @@ -56,6 +57,7 @@ const schemaWithResolvers = addResolversToSchema({ streetfindEventResolvers, giveawayEventResolvers, favoriteAnimalResolvers, + medicationEventResolvers, ), inheritResolversFromInterfaces: true, }); diff --git a/src/schema/resolvers/eventMedication.ts b/src/schema/resolvers/eventMedication.ts new file mode 100644 index 0000000..c2994cb --- /dev/null +++ b/src/schema/resolvers/eventMedication.ts @@ -0,0 +1,40 @@ +import { IResolvers } from 'graphql-tools'; +import { ValidationError } from 'apollo-server-express'; +import { + createMedicationEventQuery, + updateMedicationEventQuery +} from '../../sql-queries/eventMedication'; +import { getAuthor } from './author'; + +const medicationEventResolvers: IResolvers = { + MedicationEvent: { + author: getAuthor + }, + Mutation: { + createMedicationEvent: async (_, { input }, { + pgClient, + userId + }) => { + const dbResponse = await pgClient.query( + createMedicationEventQuery({ ...input, author: userId }) + ); + return dbResponse.rows[0]; + }, + updateMedicationEvent: async (_, { input }, { pgClient, userId }) => { + if (Object.keys(input).length < 2) { + throw new ValidationError( + 'You have to provide at least one data field when updating an entity' + ); + } + + const dbResponse = await pgClient.query( + updateMedicationEventQuery({...input, author: userId}) + + ); + + return dbResponse.rows[0]; + } + } +}; + +export default medicationEventResolvers; diff --git a/src/schema/typeDefs/eventMedication.graphql b/src/schema/typeDefs/eventMedication.graphql new file mode 100644 index 0000000..de10a18 --- /dev/null +++ b/src/schema/typeDefs/eventMedication.graphql @@ -0,0 +1,48 @@ +"Represents Medication event" +type MedicationEvent { + "Event id" + id: Int! + "Animal id" + animalId: Int! + "Event date" + dateTime: Date! + author: Author! + treatment: String! + expenses: Float + comments: String +} + +extend type Mutation { + "Create Medication event" + createMedicationEvent(input: CreateMedicationEventInput!): MedicationEvent + "Update Medication event" + updateMedicationEvent(input: UpdateMedicationEventInput!): MedicationEvent +} + +input CreateMedicationEventInput { + "Animal id, e.g. 2" + animalId: Int! + "Medication date in YYYY-MM-DD format" + dateTime: Date + "Event comments" + comments: String + "Treatment" + treatment: String! + "Event expenses" + expenses: Float +} + +input UpdateMedicationEventInput { + "Event id" + id: Int! + "Animal id, e.g. 2" + animalId: Int + "Medication date in YYYY-MM-DD format" + dateTime: Date + "Event comments" + comments: String + "Treatment" + treatment: String + "Event expenses" + expenses: Float +} diff --git a/src/sql-queries/eventMedication.ts b/src/sql-queries/eventMedication.ts new file mode 100644 index 0000000..e0c02e0 --- /dev/null +++ b/src/sql-queries/eventMedication.ts @@ -0,0 +1,37 @@ +import { QueryConfig } from 'pg'; +import { insert, update } from 'sql-bricks-postgres'; +import snakeCaseKeys from 'snakecase-keys'; + +const table = 'event_medication'; +const returnFields = 'id, treatment, expenses, date_time, animal_id, author, comments'; +export interface CreateMedicationEventData { + animalId: number + dateTime?: string | null + comments?: string | null + treatment: string + expenses?: string | null + author: string +} + +export interface UpdateMedicationEventData { + id: number + animalId?: number + dateTime?: string | null + comments?: string | null + treatment?: string + expenses?: number | null + author?: string +} + +export const createMedicationEventQuery = ( + data: CreateMedicationEventData +): QueryConfig => insert(table, snakeCaseKeys(data)) + .returning(returnFields) + .toParams(); + +export const updateMedicationEventQuery = ( + data: UpdateMedicationEventData +): QueryConfig => update(table, snakeCaseKeys(data)) + .where({ id: data.id }) + .returning(returnFields) + .toParams(); diff --git a/test/eventMedication.graphql.test.ts b/test/eventMedication.graphql.test.ts new file mode 100644 index 0000000..0ad0a15 --- /dev/null +++ b/test/eventMedication.graphql.test.ts @@ -0,0 +1,124 @@ +import supertest from 'supertest'; +import { expect } from 'chai'; +import { authorFields } from './authorFields'; +import validate from './validators/eventMedication.interface.validator'; + +require('dotenv').config({ path: './test/.env' }); + +const url = process.env.TEST_URL || 'http://localhost:8081'; +const request = supertest(url); + +const eventMedicationFields = ` + { + id, + animalId, + dateTime, + comments, + treatment, + expenses, + author ${authorFields} + } +`; + +describe('GraphQL medication event integration tests', () => { + + const expectedCreateResult = { + animalId: 4, + dateTime: '2021-10-08', + comments: '1 dose have been administered already', + treatment: 'Antibiotics 1 tablet per day for 10 days', + expenses: 35.00, + author: { + id: 'userIdForTesting', + name: 'Ąžuolas', + surname: 'Krušna' + } + }; + + it('Creates medication event with all fields', (done) => { + let req = request + .post('/graphql') + .send({ + query: `mutation { + createMedicationEvent( + input: { + animalId: 4 + dateTime: "2021-10-08" + comments: "1 dose have been administered already" + treatment: "Antibiotics 1 tablet per day for 10 days" + expenses: 35.00 + } + ) ${eventMedicationFields} + }` + }) + .expect(200); + if (process.env.BEARER_TOKEN) { + req = req.set('authorization', `Bearer ${process.env.BEARER_TOKEN}`); + } + req.end((err, res) => { + if (err) return done(err); + const { body: { data: { createMedicationEvent } } } = res; + validate(createMedicationEvent); + expect(createMedicationEvent).to.deep.include(expectedCreateResult); + return done(); + }); + }); + + const expectedUpdateResult = { + animalId: 4, + dateTime: '2021-10-07', + comments: 'Bacteria sample has been taken, results will come back in 7 days', + treatment: 'Anti-inflammatory syrup 6ml per day, 1cm vitamin gel per day with food', + expenses: 44.00, + author: { + id: 'userIdForTesting', + name: 'Ąžuolas', + surname: 'Krušna' + } + }; + + it('Update Medication event with all fields', (done) => { + let req = request + .post('/graphql') + .send({ + query: `mutation { + updateMedicationEvent ( + input: { + id: 1 + animalId: 4 + dateTime: "2021-10-07" + comments: "Bacteria sample has been taken, results will come back in 7 days" + treatment: "Anti-inflammatory syrup 6ml per day, 1cm vitamin gel per day with food" + expenses: 44.00 + } + ) { + id + animalId + dateTime + comments + treatment + expenses + author { + id + name + surname + } + } + }`, + }) + .expect(200); + if (process.env.BEARER_TOKEN) { + req = req.set('authorization', `Bearer ${process.env.BEARER_TOKEN}`); + } + req.end((err, res) => { + if (err) return done(err); + const { body: { data: { updateMedicationEvent } } } = res; + validate(updateMedicationEvent); + expect(updateMedicationEvent).to.deep.include({ + id: 1, + ...expectedUpdateResult, + }); + return done(); + }); + }); +}); diff --git a/test/interfaces/eventMedication.interface.ts b/test/interfaces/eventMedication.interface.ts new file mode 100644 index 0000000..7c31e65 --- /dev/null +++ b/test/interfaces/eventMedication.interface.ts @@ -0,0 +1,11 @@ +import Author from './author.interface'; + +export default interface MedicationEvent { + id: number + animalId: number + dateTime: string | null + comments: string | null + treatment: string + expenses: number | null + author: Author +} diff --git a/test/validators/eventMedication.interface.validator.ts b/test/validators/eventMedication.interface.validator.ts new file mode 100644 index 0000000..19c3d0a --- /dev/null +++ b/test/validators/eventMedication.interface.validator.ts @@ -0,0 +1,100 @@ +/* tslint:disable */ +// generated by typescript-json-validator +import { inspect } from 'util'; +import MedicationEvent from '../interfaces/eventMedication.interface'; +import Ajv = require('ajv'); + +export const ajv = new Ajv({"allErrors":true,"coerceTypes":false,"format":"fast","nullable":true,"unicode":true,"uniqueItems":true,"useDefaults":true}); + +ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-06.json')); + +export {MedicationEvent}; +export const MedicationEventSchema = { + "$schema": "http://json-schema.org/draft-07/schema#", + "defaultProperties": [ + ], + "definitions": { + "default": { + "defaultProperties": [ + ], + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": [ + "null", + "string" + ] + }, + "surname": { + "type": [ + "null", + "string" + ] + } + }, + "required": [ + "id", + "name", + "surname" + ], + "type": "object" + } + }, + "properties": { + "animalId": { + "type": "number" + }, + "author": { + "$ref": "#/definitions/default" + }, + "comments": { + "type": [ + "null", + "string" + ] + }, + "dateTime": { + "type": [ + "null", + "string" + ] + }, + "expenses": { + "type": [ + "null", + "number" + ] + }, + "id": { + "type": "number" + }, + "treatment": { + "type": "string" + } + }, + "required": [ + "animalId", + "author", + "comments", + "dateTime", + "expenses", + "id", + "treatment" + ], + "type": "object" +}; +export type ValidateFunction = ((data: unknown) => data is T) & Pick +export const isMedicationEvent = ajv.compile(MedicationEventSchema) as ValidateFunction; +export default function validate(value: unknown): MedicationEvent { + if (isMedicationEvent(value)) { + return value; + } else { + throw new Error( + ajv.errorsText(isMedicationEvent.errors!.filter((e: any) => e.keyword !== 'if'), {dataVar: 'MedicationEvent'}) + + '\n\n' + + inspect(value), + ); + } +}