-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathinitDb.ts
More file actions
268 lines (236 loc) · 7.32 KB
/
initDb.ts
File metadata and controls
268 lines (236 loc) · 7.32 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
import {
Field,
FieldType,
PatientTagsField,
PatientTagSyria,
ReservedStep,
RootStep,
RootStepFieldKeys,
Step,
StepStatus,
} from '@3dp4me/types'
import _ from 'lodash'
import log from 'loglevel'
import mongoose, { SchemaDefinitionProperty } from 'mongoose'
import encrypt from 'mongoose-encryption'
import { StepModel } from '../models/Metadata'
import { fileSchema } from '../schemas/fileSchema'
import { signatureSchema } from '../schemas/signatureSchema'
/**
* Initalizes and connects to the DB. Should be called at app startup.
*/
export const initDB = (callback?: () => void) => {
mongoose.connect(process.env.DB_URI!, {
// useNewUrlParser: true,
// useUnifiedTopology: true,
})
mongoose.connection
.once('open', async () => {
log.info('Connected to the DB')
await initReservedSteps()
await initModels()
callback?.()
})
.on('error', (error) => log.error('Error connecting to the database: ', error))
}
const clearModels = async () => {
const steps = await StepModel.find()
steps.forEach((step) => {
// @ts-expect-error this is a hack
delete mongoose.connection.models[step.key]
})
}
// Migrations for root step
const initReservedSteps = async () => {
log.info('Initializing the reserved step')
const rootStep = await StepModel.findOne({ key: ReservedStep.Root }).lean()
if (!rootStep) {
log.info('Creating the reserved step')
return StepModel.create(RootStep)
}
// Older version missing the tag field
const tagField = rootStep.fields.find((f) => f.key === RootStepFieldKeys.Tags)
if (!tagField) {
log.info('Tags is missing from reserved step, adding it')
return StepModel.updateOne(
{ key: ReservedStep.Root },
{ $push: { fields: PatientTagsField } }
)
}
// Older version missing the syria option
const syriaOption = tagField.options.find((o) => o.Question.EN === PatientTagSyria.Question.EN)
if (!syriaOption) {
log.info('Syria is missing from tag options, adding it')
return StepModel.updateOne(
{
key: ReservedStep.Root,
'fields.key': RootStepFieldKeys.Tags,
},
{ $push: { 'fields.$.options': PatientTagSyria } }
)
}
log.info('Reserved step is up to date')
return null
}
export const reinitModels = async () => {
await clearModels()
await initModels()
}
/**
* Initializes all of the dynamic models in the DB. Should be called immediately after initDB.
*/
export const initModels = async () => {
const steps = await StepModel.find()
steps.forEach((step) => generateSchemaFromMetadata(step))
}
/**
* Generate and registers a schema based off the provided metadata.
*/
export const generateSchemaFromMetadata = (stepMetadata: Step) => {
const stepSchema = generateFieldsFromMetadata(stepMetadata.fields, getStepBaseSchema())
const schema = new mongoose.Schema(stepSchema)
schema.plugin(encrypt, {
encryptionKey: process.env.ENCRYPTION_KEY,
signingKey: process.env.SIGNING_KEY,
excludeFromEncryption: ['patientId', 'tags'],
})
mongoose.model(stepMetadata.key, schema, stepMetadata.key)
}
/**
* Returns a list of keys that are included in the base schema for a step.
* @returns An array of strings
*/
export const getStepBaseSchemaKeys = () => {
const baseSchema = getStepBaseSchema()
const keys = Object.keys(baseSchema)
keys.push('_id')
return keys
}
/**
* Be careful modifying this base schema. We use it all over the app! Do a string search
* for the field name across the entire project to find references before changing something.
*/
const getStepBaseSchema = () => ({
patientId: { type: String, required: true, unique: true },
lastEdited: { type: Date, required: true, default: Date.now },
status: {
type: String,
required: true,
enum: Object.values(StepStatus),
default: StepStatus.UNFINISHED,
},
lastEditedBy: {
type: String,
required: true,
default: 'Admin',
},
})
/**
* Generates the schema for a given field type.
* @param {String} field Field type (see constants.js).
* @returns An object describing the field schema.
*/
export const generateFieldSchema = (field: Field): SchemaDefinitionProperty | null => {
switch (field.fieldType) {
case FieldType.STRING:
return getStringSchema()
case FieldType.MULTILINE_STRING:
return getStringSchema()
case FieldType.NUMBER:
return getNumberSchema()
case FieldType.DATE:
return getDateSchema()
case FieldType.PHONE:
return getStringSchema()
case FieldType.RADIO_BUTTON:
return getRadioButtonSchema(field)
case FieldType.FILE:
return getFileSchema()
case FieldType.PHOTO:
return getFileSchema()
case FieldType.AUDIO:
return getFileSchema()
case FieldType.FIELD_GROUP:
return getFieldGroupSchema(field)
case FieldType.SIGNATURE:
return getSignatureSchema(field)
case FieldType.HEADER:
case FieldType.DIVIDER:
return null
case FieldType.MAP:
return getMapSchema()
case FieldType.TAGS:
return getTagsSchema(field)
default:
log.error(`Unrecognized field type, ${field.fieldType}`)
return null
}
}
const getStringSchema = () => ({
type: String,
default: '',
})
const getNumberSchema = () => ({
type: Number,
default: 0,
})
const getDateSchema = () => ({
type: Date,
default: Date.now,
})
const getTagsSchema = (fieldMetadata: Field) => {
if (!fieldMetadata?.options?.length) {
throw new Error('tags must have options')
}
return {
type: [String],
default: [],
}
}
const getRadioButtonSchema = (fieldMetadata: Field) => {
if (!fieldMetadata?.options?.length) {
throw new Error('Radio button must have options')
}
return {
type: String,
default: '',
}
}
const getFieldGroupSchema = (fieldMetadata: Field) => {
// Field groups can have 0 sub fields.
if (!fieldMetadata?.subFields) {
throw new Error('Field groups must have sub fields')
}
return {
type: [generateFieldsFromMetadata(fieldMetadata.subFields)],
required: true,
default: [],
}
}
const getFileSchema = () => ({
type: [fileSchema],
default: [],
})
const getSignatureSchema = (fieldMetadata: Field) => {
const defaultURL = fieldMetadata?.additionalData?.defaultDocumentURL
if (!defaultURL?.EN || !defaultURL?.AR) {
throw new Error('Signatures must have a default document for both English and Arabic')
}
return { type: signatureSchema }
}
const getMapSchema = () => ({
type: {
latitude: Number,
longitude: Number,
},
})
const generateFieldsFromMetadata = (fieldsMetadata: Field[], baseSchema = {}) => {
const generatedSchema = fieldsMetadata.map((field) => {
const s = generateFieldSchema(field)
if (!s) return null
return {
[field.key]: s,
}
})
return Object.assign(_.cloneDeep(baseSchema), ...generatedSchema)
}