Skip to content

How to create new questionnaire component (BACKEND)

Jekabs Karklins edited this page Feb 7, 2023 · 5 revisions

This document will guide you through creating a new questionnaire component. After finishing this, you want to go and check the frontend part that's required for creating a component. https://github.com/UserOfficeProject/user-office-core/wiki/How-to-create-new-questionnaire-component-(FRONTEND)

Add new Enum to the DataType

Let's start by opening src/models/Template.ts. In the DataType enum add your new DataType. For this example, we will call our DataType INTERVAL

export enum DataType {
  BOOLEAN = 'BOOLEAN',
  DATE = 'DATE',
  EMBELLISHMENT = 'EMBELLISHMENT',
...
  INTERVAL = 'INTERVAL',
}

Create a new file for the component

Create a new file under src/models/questionTypes e.g. Interval.ts Here add the definition of the component. The definition should contain an object conforming to the interface Question residing in src/models/questionTypes/QuestionRegistry.ts

Example

export const intervalDefinition: Question = {
  dataType: DataType.INTERVAL,
  getDefaultAnswer: field => {
    return {
      min: 0,
      max: 0,
      unit: null,
    };
  },
  validate: (
    field: QuestionTemplateRelation,
    value: { min: number; max: number; unit: string | null }
  ) => {
    // it is recommended to use the validation schema for the duo-validation library so that the validation is shared between the frontend and the backend. 
    return intervalQuestionValidationSchema(field).isValid(value);
  },
  createBlankConfig: (): IntervalConfig => {
    const config = new IntervalConfig();
    config.small_label = '';
    config.required = false;
    config.tooltip = '';
    config.property = '';
    config.units = [];

    return config;
  },
  filterQuery: (queryBuilder, filter) => {
    // filterQuery is used for finding the questionnaire matching the answer
    const value = JSON.parse(filter.value).value;
    switch (filter.compareOperator) {
      case QuestionFilterCompareOperator.LESS_THAN:
        return queryBuilder.andWhereRaw(
          "(answers.answer->'value'->>'siMax')::float < ?",
          value
        );
      case QuestionFilterCompareOperator.GREATER_THAN:
        return queryBuilder.andWhereRaw(
          "(answers.answer->'value'->>'siMin')::float > ?",
          value
        );
      default:
        throw new Error(
          `Unsupported comparator for Interval ${filter.compareOperator}`
        );
    }
  }
};

Here are the fields that must be defined:

Property Type Description
dataType DataType The enum value from DataType
validate (field: QuestionTemplateRelation, value: any) => boolean Perform validation rules before persisting data into the database
createBlankConfig () => any Function that returns config initial values
getDefaultAnswer (field: QuestionTemplateRelation) => any Returns the answer value for the question that is not answered yet

After creating the component definition, register your component by adding a new entry to the registry array in src/models/questionTypes/QuestionRegistry.ts

const registry = [
  booleanDefinition,
  dateDefinition,
  embellishmentDefinition,
...
  intervalDefinition,
];

Add new data type in the database

Create a new SQL database patch in db_patches

DO
$$
BEGIN
	IF register_patch('AddIntervalDataType.sql', 'firstnamelastname', 'Adding new question type', 'xxxx-xx-xx') THEN
	BEGIN

    INSERT INTO question_datatypes VALUES('INTERVAL');

    END;
	END IF;
END;
$$
LANGUAGE plpgsql;

Add new FieldConfig

Your component will probably have unique config parameters that you want to allow the administrator to configure, and that can vary from one question type to another. To define a config add a new type in src/resolvers/types/FieldConfig.ts.

For example: If we want to be able to configure units field for our component, then it could look something like this

@ObjectType()
export class IntervalConfig extends ConfigBase {
  @Field(() => [Unit])
  units: Unit[];
}

Note that decorators in this class are not specific to the questionnaire component but rather TypeGraphQL annotations.

Next, we need to add this config to the FieldConfig GraphQL UnionType, which comes as a requirement for TypeGraphQL

export const FieldConfigType = createUnionType({ name: 'FieldConfig', types: () => [ BooleanConfig, DateConfig, EmbellishmentConfig, FileUploadConfig, ... IntervalConfig, ], });

Next steps

  • Frontend
    Now when the backend part is done, go ahead and implement the frontend part
  • PDF generation
    In order to support PDF generation with your new datatype you will also need to make adjustments to user-office-factory
  • Validation library
    It is recommended to implement a validation schema for your new component in the validation library.
  • Questionnaire ERD
    Learn more about underlying data structure of the Questionnaire