Transactional email sending sponsored by rapidmail.
This is the storage backend for Distribute Aid Needs Assessments. The main goal of this project is to provide a flexible way to
- define needs assessment forms and
- store the responses for these forms.
npm ci
npm test
npm start
ℹ️ The UI that displays forms is implemented in the needs assessment project.
Forms are a set of questions, that have to be answered. They are defined as a
JSON document and must follow this schema. The schema is
served under /schema
on the running instance.
A minimal form definition looks like this:
{
"$schema": "http://localhost:3000/schema/0.0.0-development/form#",
"sections": [
{
"id": "aboutYou",
"title": "About you",
"questions": [
{
"id": "name",
"title": "What is your name?",
"required": true,
"format": {
"type": "text"
}
}
]
}
]
}
This defines a form with one section, and the required question for the name of the user.
Sections can be hidden, and questions can be hidden and required based on
JSONata expressions. The expression will be evaluated
against the contents of the response and must evaluate to true
or false
.
This allows to hide sections of the form based on an answer in an earlier
section.
Consider this example:
{
"$schema": "http://localhost:3000/schema/0.0.0-development/form#",
"$id": "http://localhost:3000/form/example",
"sections": [
{
"id": "additional",
"title": "Additional Information",
"questions": [
{
"id": "needOtherItems",
"title": "Are there any other items you need?",
"required": true,
"format": {
"type": "single-select",
"style": "radio",
"options": [
{
"id": "yes",
"title": "yes"
},
{
"id": "no",
"title": "no"
}
]
}
},
{
"id": "otherItemsNeeded",
"title": "Please describe the other items you need:",
"hidden": "$not($exists(additional.needOtherItems)) or additional.needOtherItems = 'no'",
"required": "additional.needOtherItems = 'yes'",
"format": {
"type": "text",
"multiLine": true
}
}
]
}
]
}
It defines the single choice question additional.needOtherItems
(Are there
any other items you need?) which user can answer with yes
or no
. The
definition for the second question has a JSONata expression for both hidden
and required
. The hidden
expression will evaluate to true
(resulting in
the input field to be hidden), if no answer was given, yet (in this case the
value in the response will not be defined), or if the answer is no
. Only if
the answer is yes
, will the question be made mandatory.
In order for assessments to be stored, the form needs to be created first.
Storing above form is done by sending a POST request to /form
:
http POST http://localhost:3000/form <<< '{"$schema":"http://localhost:3000/schema/0.0.0-development/form#","sections":[{"id":"aboutYou","title":"About you","questions":[{"id":"name","title":"What is your name?","required":true,"format":{"type":"text"}}]}]}'
This will store the form as a new entry, and return the URL to it in the
location
header:
HTTP/1.1 201 Created
Location: http://localhost:3000/form/01FVZQH3NRPW38JSMD63KCM043
http://localhost:3000/form/01FVZQH3NRPW38JSMD63KCM043
must now be referenced
in the response. This will cause the response to be validated against the form.
The response is submitted to /assessment
:
http POST http://localhost:3000/assessment <<< '{"form":"http://localhost:3000/form/01FVZQH3NRPW38JSMD63KCM043","response":{"aboutYou":{"name":"Alex Doe"}}}'
If the response is valid, it will be stored locally.
Assessments will be sent to the configured admin email addresses, and include a TSV file of the assessment.
Configure the ADMIN_EMAILS
environment variable with a comma-separated list of
emails. In addition, configure the SMTP settings using environment variables for
sending out emails:
export SMTP_FROM=... # e.g. [email protected]
export SMTP_SERVER=... # e.g. example.com
export SMTP_USER=... # e.g. [email protected]
export SMTP_PASSWORD=... # e.g. secret
export SMTP_SECURE=... # e.g. false
export SMTP_PORT=... # e.g. 587
Responses cannot, and should not be edited. However it is possible for adminstrators to provide corrections. These amend responses. All corrections are stored in separate files.
http PATCH http://localhost:3000/correction 'Cookie:auth=...' <<< '{"form":"http://localhost:3000/form/01FVZQH3NRPW38JSMD63KCM043","assessment":"http://localhost:3000/assessment/01G66DFRWRCXJ2T5AZZAHD8D6T","response":{"aboutYou":{"name":"Alex Doe"}}}'
The numerical questions in a form can be summarized:
http GET http://localhost:3000/form/01FVZQH3NRPW38JSMD63KCM043/summary
The response will include unit conversion.
{
"summary": {
"foodItems": {
"rice": {
"kg": 1843
},
"cannedTomatoes": {
"cans": 2788
}
},
"hygieneItems": {
"washingDetergent": {
"washCycles": 2810
}
}
},
"stats": {
"count": 3
}
}
The summary can further be filtered by answers to any question.
- summarize only assessments for a specific region:
http GET http://localhost:3000/form/01FVZQH3NRPW38JSMD63KCM043/summary?basicInfo.region=lesvos
- summarize only assessments for a specific country:
http GET http://localhost:3000/form/01FVZQH3NRPW38JSMD63KCM043/summary?basicInfo.region=countryCode:GR
(this depends on the questionbasicInfo.region
to use theregion
question type, which is a specialized question type that has acountryCode
property). - create combinations multiple answers
http GET http://localhost:3000/form/01FVZQH3NRPW38JSMD63KCM043/summary?basicInfo.region=lesvos&timeOfYear.quarter=q2
Summaries can be grouped, by multiple answers, i.e. by quarter and by region
using
http GET http://localhost:3000/form/01FVZQH3NRPW38JSMD63KCM043/summary?groupBy=timeOfYear.quarter,basicInfo.region
will group all answers first by time of year, and further group them by region.
Given these responses
[
{
"id": "01GDP4JXFDGAWAH36HZKBMSF2N",
"response": {
"basicInfo": { "region": "samos" },
"foodItems": { "rice": [2, "epal"], "cannedTomatoes": [100, "cans"] },
"hygieneItems": { "washingDetergent": [10, "bottle1l"] },
"timeOfYear": { "quarter": "q1" }
},
"corrections": []
},
{
"id": "01GDP4JXFDTYH42VYWTNME67BA",
"response": {
"basicInfo": { "region": "lesvos" },
"foodItems": { "rice": [200, "kg"], "cannedTomatoes": [3, "epal"] },
"hygieneItems": { "washingDetergent": [10, "bag5k"] },
"timeOfYear": { "quarter": "q2" }
},
"corrections": []
},
{
"id": "01GDP4JXFEK65RF33CHAB0TABD",
"response": {
"basicInfo": { "region": "calais" },
"foodItems": { "rice": [123, "kg"], "cannedTomatoes": [4, "epal"] },
"hygieneItems": { "washingDetergent": [17, "bag5k"] },
"timeOfYear": { "quarter": "q2" }
},
"corrections": []
}
]
using the grouping definition from above, the result will be:
{
"summary": {
"q1": {
"samos": {
"foodItems": {
"rice": { "kg": 1520 },
"cannedTomatoes": { "cans": 100 }
},
"hygieneItems": { "washingDetergent": { "washCycles": 380 } }
}
},
"q2": {
"lesvos": {
"foodItems": {
"rice": { "kg": 200 },
"cannedTomatoes": { "cans": 1152 }
},
"hygieneItems": { "washingDetergent": { "washCycles": 900 } }
},
"calais": {
"foodItems": {
"rice": { "kg": 123 },
"cannedTomatoes": { "cans": 1536 }
},
"hygieneItems": { "washingDetergent": { "washCycles": 1530 } }
}
}
},
"stats": { "count": 3 }
}
Forms and responses are stored on the local filesystem. When using Clever Cloud, file system buckets are used and mounted at deploy time to store the JSON files with the forms and responses.
The mount point on the production instance is configured via the
CC_FS_BUCKET
environment variable
of the instance.