1.2.0 • Published 2 years ago

uask-dom v1.2.0

Weekly downloads
-
License
MIT
Repository
github
Last release
2 years ago

U-ASK

U-ASK Domain Model

U-ASK domain model classes and builders. This library implement an internal domain specific language as described in Martin Fowler's Domain Specific Languages.

This library is intended to be used with U-ASK Management System and U-ASK Web Application.

Note: npm scripts needs bash ; please configure git bash as script shell for npm on Windows.

Install

npm Install uask-dom

Usage

Survey construction

A Survey is composed of Page objects. Page objects are hierarchical, they include other Page objects or PageItems leaf objects.

Page objects are grouped in PageSet objects that represent questionnaires. A Survey may have multiple questionnaires.

PageItem represent a question, they have a wording and a type. They may be involved in Rule objects that control the value of question answers.

Survey class diagram

Survey objects are complex graphes, construction is easier with fluent builders :

import { builder } from "uask-dom"

const b = builder();

b.survey("First-Survey")
  .pageSet("Questionnaire").pages("Questions");

b.page("Questions")
  .question("Ok?", "OK", b.types.yesno)
  .question("When:", "WHEN", b.types.date());

const survey = b.build();

The result is a Survey object with PageSet, Page and PageItem objects that reflects the stucture of the fluent program above.

The complete refercence for survey buiders is below

Rules

A PageItem may be the target or a Rule. For example a page item can enforce that a value must be provided.

b.page("Questions")
  .question("Ok?", "OK", b.types.yesno)
  .required()
  // ...

A PageItem value can have a default value :

b.page("Questions")
  // ...
  .question("When:", "WHEN", b.types.date())
  .defaultValue("@TODAY");

Rules are executed in order of appearance of their target PageItem in the DSL ; for a given target they are executed in order of deacreasing precedence.

rule namedescriptionparametersprecedence
copycopy another item value, unit, etc.variable name110
computedcacluate the value from other itemsformula100
defaultValuethe value when the item is activatedvalue*100
criticalfires an event when item is valuedevent, message, values70
requiredrequires a value when enforcedformula?70
activateWhenactivate the item on conditionvariableName, values OR formula50
decimalPrecisionthe number of decimal digitsprecision10
inRangemust be between given min and maxmin, max, limits10
letterCaseenforce lower or upper case"upper" OR "lower"10
fixedLengththe exact number of characterslength10
maxLengththe max number of characterslength10

note: parameters followed by a * may be replaced by a formula, a formula is constructed wih the computed keyword:

b.page("Questions")
  // ...
  .question("When:", "WHEN", b.types.date())
  .inRange("2022-01-01", b.computed("@TODAY"));

A critical rule fires an event that will be notified to users which workflow subscribed to that event (see Derived workflows).

b.page("Questions")
  // ...
  .question("When:", "WHEN", b.types.date())
  .critical("before_2022", "before 2022", b.computed("WHEN < #2022-01-01#"));

Workflows

A Survey has one or more Workflow objects. A workflow represents the page sets a user has access to and when he can create an interview related to a given page set.

Main workflow

In the main workflow a page set may :

  • appear once or multiple times
  • belong to the sequence or be auxiliary
  • terminate the workflow (or not)

The workflow sequence follows the following stucture:

workflow sequence

The fisrt interview of a participant always refercences the starting page set. Subsequent interviews will reference pages sets in order defined in the sequence. Initial page sets appears in only one interview and follow up page sets can be repeated in multiple interviews, always following the sequence.

Terminal page sets terminate the workflow: no more interview could be created for the participant.

Auxiliary page sets can appear any time after the sequence started.

Workflow creation is part of Survey construction:

import { builder } from "uask-dom"

const b = builder();

b.survey("First-Survey")
  .pageSet("Initial 1").pages("I1")
  .pageSet("Initial 2").pages("I2")
  .pageSet("Follow up 1").pages("F1")
  .pageSet("Follow up 2").pages("F2")
  .pageSet("Terminal 1").pages("T1")
  .pageSet("Terminal 2").pages("T1")
  .pageSet("Auxiliary 1").pages("A1")
  .pageSet("Auxiliary 2").pages("A2")

//...

b.workflow()
  .initial("Initial 1", "Initial 2")
  .followUp("Follow up 1", "Follow up 2")
  .terminal("Terminal 1", "Terminal 2")
  .auxiliary("Auxiliary 1", "Auxiliary 2")

const survey = b.build();

Derived workflows

Derived workflows are built from the main workflow, giving access to a restricted subset of the page sets. They have the same sequence, terminal and auxiliary page sets.

Derived workflows can optionally have associated notifications. Notifications refers to critical events declared with a question. Users with that workflow will receive notifications when the event fires.

A derived workflow is identified by its name (which can not be "main"), and optionally a specifier. Names are not restricted in this library but in U-ASK Management System the allowed values are :

  • writer
  • reader
  • administrator
  • superadministrator
  • developer The specifier if any, is positionned after the name, separated with : (e.g. "writer:investigator", "reader:coordinator").
// ...

b.workflow("writer:investigator")
  .withPageSets("Follow up 1", "Follow up 2")
  .notify("special")

Participant construction

A Participant participes to a Survey ; it belongs to a Sample.

Participant contains Interview objects that reference a PageSet and contains InterviewItem objects.

InterviewItem holds a value and represent an answer to a PageItem (a question).

Participant may also be build using fluent buiders.

The following code builds a Participant with code "11A" in the given sample of the given survey. The participant will have one interview, corresponding to page set "Questionnaire" with two answers, for items corresponding to variables "OK" and "WHEN".

import { ParticipantBuilder } from "uask-dom";

const builder = new ParticipantBuilder(survey, "11A", sample);
builder.interview("Questionnaire")
  .item("OK").value(false)
  .item("WHEN").value(new Date());
const participant = builder.build();

the same can be done using domain model objects instead of string identifiers:

import { ParticipantBuilder } from "uask-dom";

const questionnaire: PageSet = //...
const Ok: PageItem = //...
const When: PageItem = //...

const builder = new ParticipantBuilder(survey, "11A", sample);
builder.interview(questionnaire)
  .item(Ok).value(false)
  .item(When).value(new Date())
const participant = builder.build();

See ./src/example/index.ts for an example of fluent builders usage.

Domain model objects mutations

Domain model objects are mostly immutable. When a domain model object needs an update in some business logic, a new version of the object is produced :

const pageItem = new PageItem("Are you OK ?", "OK", QuestionTypes.yesno);
const interviewItem = new InterviewItem(pageItem, true);
//...
const updatedInterviewItem = interviewItem.update({ value: false });

Domain object collections are also immutable, if a new version of an object has been produced, a new collection including the new object must be created :

const interviewItems = DomainCollection(interviewItem, ...);
//...
const updatedInterviewItems = interviewItems.update(
  a => a == interviewItem ? updatedInterviewItem : a
);

Mutations on Patient, Interview and InterviewItem can be achieved using the corresponding builders. The following snippet will created and updated version the participant, with items corresponding to variables "OK" and "WHEN" updated in interview corresponding do page set Questionnaire and identified by the nonce 11145786.

import { ParticipantBuilder } from "uask-dom";

const participant: Patient = //...

const builder = new ParticipantBuilder(survey, participant)
builder.interview("Questionnaire", 11145786)
  .item("OK").value(false)
  .item("WHEN").value(new Date())
const updatedPatient = builder.build();

The same can be done with domain models instead of string identifiers :

import { ParticipantBuilder } from "uask-dom";

const participant: Patient = //...
const questionnaire11145786: Interview = //...
const Ok: PageItem = //...
const When: PageItem = //...

const builder = new ParticipantBuilder(survey, participant)
builder.interview(questionnaire11145786)
  .item(Ok).value(false)
  .item(When).value(new Date())
const updatedPatient = builder.build();

DSL reference

Survey and visit construction

instruction
.survey(name)creates a survey with given name
.options(opt)declares options for the survey
.visit(name)creates a page set (or visit) with given name
.translate(lang, name)add a translation for visit name
.dateVariable(name)declares the variable which holds the visit date
.pages(names...)adds pages with given names to the visit
.mandatory(name)declares that a page is mandatory in a visit, result is passed to .pages

Survey options

optiondefault value
languages['en', 'fr']languages supported by the survey
defaultLang'en'fallback language if browser language is not supported
showFillRatetrueshow fill rate for visits
visitDateVar'VDATE'default variable that holds the visit date
phoneVar'__PHONE'participant phone number if any
emailVar'__EMAIL'participant email if any
inclusionVar'__INCLUDED'variable that holds whether the participant is included or not
unitSuffix'_UNIT'suffix used for exporting data units if applicable
Example
b.survey('Demo-eCRF')
  .defaultLang('fr')
  .visit('INCL')
    .translate('fr', 'Inclusion')
    .translate('en', 'Inclusion')
    .dateVariable('DATE_VIS')
    .pages(b.mandatory('PATIENT_INFO'), 'ANTECED')

Page and question construction

instruction
.page(name)creates a page with given name
.translate(lang, name)adds a translation for page name
.startSection(name)declares a section with given name
.translate(lang, name)adds a translation for section name
.question(wording, variable, types...)adds a question with given wording, variable and types (type can change with context, see below)
.translate(lang, wording)adds a translation for question wording
.comment(info)adds information about the variable
.translate(lang, info)adds a translation for question comment
.unit(units...)declares that the variable value may be expressed in one of the given units
.extendable()declares that custom units may be used for the variable
.defaultValue(value)declares that a new variable instance is filled with given value
.required()declares that the variable cannot be filled by a special value
.decimalPrecision(precision)decrares that the variable is a number with at most given precision decimal digits
.inRange(min, max, limits)decrares that the variable value must be between given values, limits may or may not be included (see below)
.maxLength(length)decrares that the variable value is a text which lenght is at most given length
.fixedLength(length)decrares that the variable value is a text which lenght is exactly given length
.letterCase(case)decrares that the variable value is a text which case is 'upper' or 'lower'
.computed(formula)apply the given formula to the variable
.critical(event, message, values...)fires the event when the item receives the given values
.critical(event, message, formula)fires the event when the formula is true
.visibleWhen(formula, results...)shows this question only when the given formula is true or if any equal to one of the given results
.activatedWhen(formula, results...)activates this question only when the given formula is true or if any equal to one of the given results
.modifiableWhen(formula, results...)allows modifications on this question only when the given formula is true or if any equal to one of the given results
.question(variable, types...)adds a question with given variable and types (type can change with context, see below), allow multiple wordings
.wordings(wordings...)adds multiple, contextual wordings (see below)
.translate(lang, wordings...)adds a translation for multiple wordings
.include(name)includes all the question of the given page
.context(variable, num)switch included variable to context num (i.e. with corresponding wording and type if multiple are declared)
.context(num)switch all included variables to context num (i.e. with corresponding wording and type if multiple are declared)
.visibleWhen(formula, results...)shows the included questions only when the given formula is true or if any equal to one of the given results
.activatedWhen(formula, results...)activates the included questions only when the given formula is true or if any equal to one of the given results
.modifiableWhen(formula, results...)allows modifications on the included questions only when the given formula is true or if any equal to one of the given results
.computed(formula)declares a dynamic constraint, result is passed inRange, activatedWhen, etc.
.copy(variable)declares a copy an existing variable values, result is passed defaultValue
.includeLimitsdeclares that an in range constraint must include limits, result is passed to inRange
.includeUpperdeclares that an in range constraint must include upper limit, result is passed to inRange
.includeLowerdeclares that an in range constraint must include lower limit, result is passed to inRange

Variable types

The third argument of .question(wording, variable, type) is the result of one of

instructionVariable type
.types.acknowledgetrue or undefined
.types.yesnoYes or No (translated to user language)
.types.integera natural number
.types.reala real number
.types.texta text
.types.infono value
.types.date(incomplete?, month?)a date optionally incomplete and truncted to month
.types.scale(min, max)an integer value between min and max
.types.score(scores...)an integer that is used to compute scores
.wording(lang, wordings...)declares wording for scores
.translate(lang, wordings...)adds a translation for score wordings
.types.choice('one' | 'many', choices...)a set of categories that can be exclusive (one) or not (many)
.wording(lang, wordings...)declares wording for categories
.translate(lang, wordings...)adds a translation for category wordings
.types.glossary('one' | 'many', choices...)a set of categories that allows custom values and can be exclusive (one) or not (many)
.wording(lang, wordings...)declares wording for categories
.translate(lang, wordings...)adds a translation for category wordings
Example
b.page('INCL')
  .translate('fr', 'Inclusion')
  .translate('en', 'English')
  .question('Date de la visite', 'DATE_VIS', b.types.date())
    .translate('en', 'Visit date')
    .required()
    .inRange('#2000-01-01#', b.computed('@TODAY'))
  .question('Sexe', 'SEX',
    b.types.choice('one', 'H', 'F', 'ND')
      .wording('Homme', 'Femme', 'Non déterminé')
      .translate('en', 'Male', 'Female', 'Undetermined'))
    )
    .translate('en', 'Sex')

Computed formulas

A formula are composed of :

  • operators

    operator
    + - * /arithmetic operators
    < > <= >= == !=comparison operators
    ? ... : ...ternary operator
    && || == !=logical operators
    (...)sub expressions
  • values

    value
    0, 1, 2 ...numbers (may be decimals)
    '...'or "..."texts
    #YYYY-MM-DD#dates
  • variables

    variable
    X, Y, Z ...current value of a variable declared in the questions
    $X, $Y, $Z ...previous value of a variable declared in the questions
    @LASTINlast input date for the participant
    @UNDEFundefined value
    @ACKacknowledged or true value
    @THISYEARcurrent year
    @TODAYcurrent date
  • functions

    function
    ~IN(VAR, value)returns @ACK if given variable of type .choice('many') contains the given value
Examples
b.page('...')
 .question('Où souffrez-vous ?', 'LOC', b.types.choice('many', 'MAIN', 'DOS', 'JAMBE', 'TÊTE', 'AUTRE')
 .question('Si AUTRE, précisez', 'LOC_AUTRE', b.types.text)
   .activatedWhen(~IN('LOC, "Autre")')
b.page('...')
  question('Date de la visite', 'DATE_VIS', b.types.date())
 .question('Confirmez que la date est postérieure à ce jour', '__DATE_POST', b.types.acknowledge)
   .required()
   .visibleWhen('DATE_VIS>@TODAY')

Question layouts

Questions that are related may be viewed as tables or recordsets

A table represents variables that can be arranged as

column Acolmumn B
row1A1B1
row2A2B2

A recordset represents variables that can have multiple instances in the same form

column Acolmumn B
A(1)B(1)
A(2)B(2)
......

This is achieved by using DSL syntax in the question wordings :

wordinglayout
row -> columntable
-> columnrecordset
-> row -> columna table nested in a recordset
Examples
b.page('...')
  .question('Red cells -> Result', 'RES_BLOOD', b.types.real)
  .question('Red cells -> Interpretation', 'INT_BLOOD', b.choice('one', 'normal', 'abnormal'))
  .question('Haemoglobin -> Result', 'RES_BLOOD', b.types.real)
  .question('Haemoglobin -> Interpretation', 'INT_HAEMO', b.choice('one', 'normal', 'abnormal'))
RésultInterpretation
Red cellsRES_BLOODINT_BLOOD
HaemoglobinRES_BLOODINT_HAEMO
b.page('...')
  .question('-> Year', 'YEAR', b.types.integer)
  .question('-> Treatment', 'TREAT', b.types.text)
  .question('-> Dosage', 'DOS', b.types.text)
YEARTreatmentDosage
YEAR(1)TREAT(1)DOS(1)
YEAR(2)TREAT(2)DOS(2)
.........

Informationnal classes and directives

The question comments can be used to add information to a question. Directives gives richer wording capabilities and classes are question categories indications. Comment can be written as : directives...{classes...}. Classes begins with a dot .

directive or class
(...)simple information
<... | ...>left and right labels
.rowthe categories should be disposed on a row
.columnthe categories should be disposed on a column
.pad or .pad1pad the question on the left
.pad2pad the question on the left with 2 levels
.pad3pad the question on the left with 3 levels
.no-specialsthe field won't be modifiable, thus do not display specials
Example
b.page(' ')
 .question('Pain scale', 'PAIN', b.types.scale(1, 10))
 .comment('<No pain | Maximum pain>{.row}')

Pain scale No pain   1 2 3 4 5 6 7 8 9 10   Maximum pain

1.2.0

2 years ago

1.1.3

2 years ago

1.1.2

2 years ago

1.1.1

2 years ago

1.1.0

2 years ago

1.0.1

2 years ago

1.0.0

2 years ago

0.2.0

2 years ago

0.1.0

2 years ago