@becollective/utils 1.10.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/.semaphore/utils.yml +1 -1
  2. package/index.ts +7 -0
  3. package/lib/FeatureFlag.d.ts +12 -0
  4. package/lib/FeatureFlag.js +51 -0
  5. package/lib/constants/allowedHtmlTags.d.ts +7 -0
  6. package/lib/constants/allowedHtmlTags.js +8 -0
  7. package/lib/constants/avatarColours.d.ts +35 -0
  8. package/lib/constants/avatarColours.js +36 -0
  9. package/lib/constants/countries.d.ts +253 -0
  10. package/lib/constants/countries.js +254 -0
  11. package/lib/constants/currencies.d.ts +29 -0
  12. package/lib/constants/currencies.js +36 -0
  13. package/lib/constants/customFields.d.ts +11 -0
  14. package/lib/constants/customFields.js +12 -0
  15. package/lib/constants/distancesUnits.d.ts +11 -0
  16. package/lib/constants/distancesUnits.js +13 -0
  17. package/lib/constants/groups.d.ts +13 -0
  18. package/lib/constants/groups.js +85 -0
  19. package/lib/constants/insuranceStatuses.d.ts +15 -0
  20. package/lib/constants/insuranceStatuses.js +7 -0
  21. package/lib/constants/insuranceTypes.d.ts +35 -0
  22. package/lib/constants/insuranceTypes.js +36 -0
  23. package/lib/constants/languages.d.ts +2 -0
  24. package/lib/constants/languages.js +194 -0
  25. package/lib/constants/locales.d.ts +119 -0
  26. package/lib/constants/locales.js +191 -0
  27. package/lib/constants/notifications.d.ts +47 -0
  28. package/lib/constants/notifications.js +31 -0
  29. package/lib/constants/opportunities.d.ts +72 -0
  30. package/lib/constants/opportunities.js +82 -0
  31. package/lib/constants/qualificationTypes.d.ts +4 -0
  32. package/lib/constants/qualificationTypes.js +18 -0
  33. package/lib/constants/rules.d.ts +13 -0
  34. package/lib/constants/rules.js +21 -0
  35. package/lib/constants/subscriptions.d.ts +9 -0
  36. package/lib/constants/subscriptions.js +10 -0
  37. package/lib/constants/surveys.d.ts +16 -0
  38. package/lib/constants/surveys.js +17 -0
  39. package/lib/constants.d.ts +1 -0
  40. package/lib/constants.js +76 -0
  41. package/lib/date-time.d.ts +28 -0
  42. package/lib/date-time.js +79 -0
  43. package/lib/forms.d.ts +1 -0
  44. package/lib/forms.js +728 -0
  45. package/lib/locality.d.ts +5 -0
  46. package/lib/locality.js +15 -0
  47. package/lib/money.d.ts +7 -0
  48. package/lib/money.js +32 -0
  49. package/lib/opportunity.d.ts +1 -0
  50. package/lib/opportunity.js +11 -0
  51. package/lib/opportunityUser.d.ts +8 -0
  52. package/lib/opportunityUser.js +29 -0
  53. package/lib/password.d.ts +8 -0
  54. package/lib/password.js +43 -0
  55. package/lib/rrule/date.d.ts +4 -0
  56. package/lib/rrule/date.js +30 -0
  57. package/lib/rrule/index.d.ts +2 -0
  58. package/lib/rrule/index.js +18 -0
  59. package/lib/rrule/parsing.d.ts +6 -0
  60. package/lib/rrule/parsing.js +130 -0
  61. package/lib/rrule/parsing.test.d.ts +1 -0
  62. package/lib/rrule/parsing.test.js +11 -0
  63. package/lib/rrule.d.ts +49 -0
  64. package/lib/rrule.js +303 -0
  65. package/lib/src/FeatureFlag.d.ts +12 -0
  66. package/lib/src/FeatureFlag.js +51 -0
  67. package/lib/src/date-time.d.ts +28 -0
  68. package/lib/src/date-time.js +79 -0
  69. package/lib/src/forms.d.ts +1 -0
  70. package/lib/src/forms.js +728 -0
  71. package/lib/src/locality.d.ts +5 -0
  72. package/lib/src/locality.js +15 -0
  73. package/lib/src/money.d.ts +7 -0
  74. package/lib/src/money.js +32 -0
  75. package/lib/src/opportunity.d.ts +1 -0
  76. package/lib/src/opportunity.js +11 -0
  77. package/lib/src/opportunityUser.d.ts +8 -0
  78. package/lib/src/opportunityUser.js +29 -0
  79. package/lib/src/password.d.ts +8 -0
  80. package/lib/src/password.js +43 -0
  81. package/package.json +15 -29
  82. package/src/FeatureFlag.ts +26 -10
  83. package/src/constants/allowedHtmlTags.ts +6 -0
  84. package/src/constants/avatarColours.ts +34 -0
  85. package/src/constants/countries.ts +252 -0
  86. package/src/constants/currencies.ts +33 -0
  87. package/src/constants/customFields.ts +10 -0
  88. package/src/constants/distancesUnits.ts +11 -0
  89. package/src/constants/groups.ts +83 -0
  90. package/src/constants/insuranceStatuses.ts +5 -0
  91. package/src/constants/insuranceTypes.ts +34 -0
  92. package/src/constants/languages.ts +192 -0
  93. package/src/constants/locales.ts +189 -0
  94. package/src/constants/notifications.ts +30 -0
  95. package/src/constants/opportunities.ts +84 -0
  96. package/src/constants/qualificationTypes.ts +17 -0
  97. package/src/constants/rules.ts +22 -0
  98. package/src/constants/subscriptions.ts +8 -0
  99. package/src/constants/surveys.ts +15 -0
  100. package/src/constants.ts +40 -0
  101. package/src/{date-time.js → date-time.ts} +18 -11
  102. package/src/forms.ts +807 -0
  103. package/src/{opportunityUser.js → opportunityUser.ts} +5 -1
  104. package/src/{password.js → password.ts} +2 -0
  105. package/src/rrule/date.ts +25 -0
  106. package/src/rrule/index.ts +2 -0
  107. package/src/rrule/parsing.ts +136 -0
  108. package/src/rrule.ts +367 -0
  109. package/tests/{date-time.test.js → date-time.test.ts} +48 -46
  110. package/tests/featureFlag.test.ts +36 -39
  111. package/tests/forms.test-data.json +43 -0
  112. package/tests/forms.test-schema.json +144 -0
  113. package/tests/forms.test-uischema.json +117 -0
  114. package/tests/forms.test.ts +674 -0
  115. package/tests/{money.test.js → money.test.ts} +2 -3
  116. package/tests/{password.test.js → password.test.ts} +7 -6
  117. package/tests/rrule/parsing.test.ts +12 -0
  118. package/tests/rrule.test.ts +625 -0
  119. package/tsconfig.json +15 -12
  120. package/bundle.js +0 -270
  121. package/index.js +0 -25
  122. package/rollup.config.js +0 -25
  123. /package/src/{locality.js → locality.ts} +0 -0
  124. /package/src/{money.js → money.ts} +0 -0
  125. /package/src/{opportunity.js → opportunity.ts} +0 -0
package/src/forms.ts ADDED
@@ -0,0 +1,807 @@
1
+ /* eslint-disable no-prototype-builtins */
2
+ import Ajv from 'ajv';
3
+ const ajv: any = new Ajv({
4
+ allErrors: true,
5
+ verbose: true,
6
+ });
7
+
8
+ type ValidateDataNameError = {
9
+ name: string;
10
+ key: string;
11
+ value: any;
12
+ }
13
+
14
+ type ValidateDataPropertyError = {
15
+ property: string;
16
+ message: string;
17
+ key: string;
18
+ }
19
+
20
+ const phoneNumber = require('libphonenumber-js');
21
+
22
+ const BC_REPORTABLE_FIELDS = {
23
+ BC_ADDRESS: 'BC_ADDRESS',
24
+ BC_PHONE: 'BC_PHONE',
25
+ BC_OWN_VEHICLE: 'BC_OWN_VEHICLE',
26
+ BC_DRIVERS_LICENCE: 'BC_DRIVERS_LICENCE',
27
+ BC_WORKING_WITH_CHILDREN: 'BC_WORKING_WITH_CHILDREN',
28
+ BC_POLICE_CHECK: 'BC_POLICE_CHECK',
29
+ BC_MEDICAL_CONDITIONS: 'BC_MEDICAL_CONDITIONS',
30
+ BC_COUNTRY_OF_BIRTH: 'BC_COUNTRY_OF_BIRTH',
31
+ };
32
+
33
+ const widgetTypes = [
34
+ 'WidgetDate',
35
+ 'WidgetDateRange',
36
+ 'WidgetDropDown',
37
+ 'WidgetHeading',
38
+ 'WidgetSpacer',
39
+ 'WidgetLocation',
40
+ 'WidgetNumber',
41
+ 'WidgetPhone',
42
+ 'WidgetRadio',
43
+ 'WidgetTextArea',
44
+ 'WidgetTextBox',
45
+ 'WidgetTextParagraph',
46
+ 'WidgetTickbox',
47
+ 'WidgetToggle',
48
+ 'WidgetSkillsRequired',
49
+ 'WidgetFile',
50
+ 'WidgetQualifications',
51
+ 'WidgetCountry',
52
+ 'WidgetSocialPrescribing',
53
+ ];
54
+
55
+ const countDecimals = (value) => {
56
+ if (Math.floor(value) === value) return 0;
57
+
58
+ const str = value.toString();
59
+ // This handles scientific notation on long decimals from toString ie "1e-9"
60
+ if (str.indexOf('.') !== -1 && str.indexOf('-') !== -1) {
61
+ return parseInt(str.split('-')[1] || 0);
62
+ }
63
+ else if (str.indexOf('.') !== -1) {
64
+ return parseInt(str.split('.')[1].length || 0);
65
+ }
66
+ return parseInt(str.split('-')[1] || 0);
67
+ };
68
+
69
+ const isValidYear = (value) => {
70
+ const year = new Date(value).getFullYear();
71
+ return year < 9999 && year > 999;
72
+ };
73
+
74
+ const validateType = async (params: { type: string, data: any, schema?: any, error?: string }) => {
75
+ const { type, data, schema, error } = params;
76
+ if (validator.dataTypeIsValid[type](data, schema)) {
77
+ return;
78
+ }
79
+ throw {
80
+ name: error || `Property is not a ${type}`
81
+ };
82
+ };
83
+
84
+ const validator = {
85
+ validateSchema: async (schema, uischema) => {
86
+ if (!schema.hasOwnProperty('properties')) {
87
+ throw {
88
+ name: 'missing-properties'
89
+ };
90
+ }
91
+ else if (!schema.hasOwnProperty('required')) {
92
+ throw {
93
+ name: 'missing-required'
94
+ };
95
+ }
96
+
97
+ let err;
98
+ let result;
99
+ const errors: any[] = [];
100
+ const keys = Object.keys(schema.properties);
101
+
102
+ result = validator.validateSchemaPropertiesExist(schema.required, keys, 'required-item-not-found');
103
+
104
+ if (result) {
105
+ throw result;
106
+ }
107
+
108
+ result = validator.validateSchemaPropertiesExist(uischema['ui:order'], keys, 'order-item-not-found');
109
+ if (result) {
110
+ throw result;
111
+ }
112
+
113
+ result = validator.validateSchemaPropertiesExist(keys, Object.keys(uischema), 'ui-data-schema-mismatch');
114
+ if (result) {
115
+ throw result;
116
+ }
117
+
118
+ result = validator.validatePropertiesTitleUniqueness(schema.properties, 'Titles must be unique.');
119
+ if (result) {
120
+ throw result;
121
+ }
122
+
123
+ for await (const item of keys) {
124
+ const o = schema.properties[item];
125
+ /*
126
+ o is each property. eg.
127
+ {
128
+ type: 'array',
129
+ title: 'Where did you hear about us?',
130
+ items: {
131
+ type: 'string',
132
+ enum: [ 'Newspaper', 'Google', 'Word of mouth' ]
133
+ },
134
+ uniqueItems: true,
135
+ bcReportable: 'BC_HOW_DID_YOU_HEAR',
136
+ bcRequired: false
137
+ }
138
+ */
139
+ const f = validator.getValidatorForType(o.bcType || o.type);
140
+
141
+ if (o.hasOwnProperty('bcReportable')) {
142
+ if (!BC_REPORTABLE_FIELDS[o.bcReportable] && !o.bcReportable.startsWith('USER_')) {
143
+ errors.push({
144
+ name: 'invalid-reportable-field',
145
+ object: o,
146
+ });
147
+ }
148
+ }
149
+
150
+ if (f) {
151
+ const valid = ajv.validate(f, o);
152
+ if (valid !== true) {
153
+ const errorMsgs = ajv.errors.map(({ instancePath, message }) => {
154
+ let property = instancePath.replace(/\//g, '.');
155
+ if (property.charAt(0) === '.') {
156
+ property = property.slice(1, property.length);
157
+ }
158
+ return {
159
+ key: item,
160
+ property,
161
+ message,
162
+ };
163
+ });
164
+ errors.push(...errorMsgs);
165
+ }
166
+ continue;
167
+ }
168
+ else {
169
+ errors.push({ name: 'invalid-schema-type', object: o });
170
+ continue;
171
+ }
172
+ }
173
+
174
+ if (errors.length) {
175
+ throw errors;
176
+ }
177
+
178
+ Object.keys(uischema).map((o) => {
179
+ if (o === 'ui:order') {
180
+ return;
181
+ }
182
+
183
+ const data = uischema[o];
184
+ const valid = ajv.validate(validator.uiSchema, data);
185
+ if (valid !== true) {
186
+ errors.push({ name: 'invalid-uischema-property', object: o });
187
+ }
188
+ });
189
+
190
+ if (errors.length) {
191
+ throw errors;
192
+ }
193
+
194
+ return true;
195
+ },
196
+
197
+ validateSchemaPropertiesExist: (properties, keys, err) => {
198
+ const errors: (ValidateDataNameError | ValidateDataPropertyError)[] = [];
199
+
200
+ for (const i in properties) {
201
+ if (keys.indexOf(properties[i]) === -1) {
202
+ errors.push({
203
+ name: err,
204
+ key: i,
205
+ value: properties[i],
206
+ });
207
+ }
208
+ }
209
+
210
+ if (errors.length) {
211
+ return errors;
212
+ }
213
+
214
+ return null;
215
+ },
216
+
217
+ /**
218
+ * Check if every field in the form has a unique title
219
+ * @param {object} properties fields properties
220
+ * @param {string} err error name
221
+ */
222
+ validatePropertiesTitleUniqueness: (properties, err) => {
223
+ const errors: (ValidateDataNameError | ValidateDataPropertyError)[] = [];
224
+ const titleCounts = {};
225
+ for (const key in properties) {
226
+ // "spacer" and "textParagraph" are special fields whose title is fixed and
227
+ // doesn't show in UI, hence no unique check required for them
228
+ if (
229
+ properties[key].bcType &&
230
+ (properties[key].bcType === 'spacer' || properties[key].bcType === 'textParagraph')
231
+ ) {
232
+ continue;
233
+ }
234
+ const title = properties[key].title.trim().toLowerCase();
235
+ titleCounts[title] = titleCounts[title] ? [...titleCounts[title], key] : [key];
236
+ }
237
+ for (const title in titleCounts) {
238
+ if (titleCounts[title].length > 1) {
239
+ titleCounts[title].forEach((key) => {
240
+ errors.push({
241
+ key,
242
+ property: key,
243
+ message: err,
244
+ });
245
+ });
246
+ }
247
+ }
248
+ if (errors.length) {
249
+ return errors;
250
+ }
251
+ return null;
252
+ },
253
+
254
+ /**
255
+ * Creates a new object from the input obj.
256
+ * All entries from the input whose values are empty arrays are missing in the new object
257
+ * @param {Object} data 1-depth obj
258
+ * @returns A new object without empty arrays
259
+ */
260
+ stripEmptyArraysFromData: (data) => {
261
+ const strippedObject = {};
262
+ Object.keys(data).forEach((k) => {
263
+ if (!(Array.isArray(data[k]) && data[k].length === 0)) strippedObject[k] = data[k];
264
+ });
265
+ return strippedObject;
266
+ },
267
+
268
+ validateData: async (schema, data) => {
269
+ // data is the user submitted form data.
270
+ const keys = Object.keys(data);
271
+ const properties = Object.keys(schema.properties);
272
+ const errors: (ValidateDataNameError | ValidateDataPropertyError)[] = [];
273
+
274
+ keys.map((o) => {
275
+ const found = false;
276
+ if (properties.indexOf(o) === -1) {
277
+ errors.push({
278
+ name: 'properties-not-found',
279
+ key: o,
280
+ value: data[o],
281
+ });
282
+ }
283
+ });
284
+
285
+ schema.required.map((o) => {
286
+ const isFileType = schema.properties[o].bcType === 'file';
287
+
288
+ if (keys.indexOf(o) === -1 && !isFileType) {
289
+ errors.push({
290
+ name: 'required-data-not-found',
291
+ key: o,
292
+ value: data[o],
293
+ });
294
+ }
295
+ });
296
+
297
+ if (errors.length) {
298
+ throw errors;
299
+ }
300
+
301
+ for await (const item of properties) {
302
+ if (!data.hasOwnProperty(item)) {
303
+ continue;
304
+ }
305
+
306
+ // property is the schema property.
307
+ // eg. { title: 'Phone Number', type: 'string', bcType: 'phone' };
308
+ const property = schema.properties[item];
309
+ const value = data[item];
310
+ // value is the data the user submits for the above property
311
+ // eg. +61434111222
312
+
313
+ try {
314
+ const v: Function = validator.getValidatorForTypeData(property.bcType || property.type) as Function;
315
+ await v(property, value);
316
+ }
317
+ catch(e) {
318
+ errors.push(e as unknown as any);
319
+ }
320
+ }
321
+
322
+ if (errors.length) {
323
+ throw(errors);
324
+ }
325
+
326
+ return;
327
+ },
328
+
329
+ getValidatorForType: (type) => {
330
+ return validator.schemaTypes[type] || null;
331
+ },
332
+
333
+ getValidatorForTypeData: (type) => {
334
+ const v = validator.dataTypes[type];
335
+ if(v) {
336
+ return v;
337
+ }
338
+ throw {
339
+ name: 'Datatype not found',
340
+ };
341
+ },
342
+
343
+ uiSchema: {
344
+ type: 'object',
345
+ required: ['ui:title', 'ui:widget'],
346
+ properties: {
347
+ 'ui:title': {
348
+ type: 'string',
349
+ },
350
+ 'ui:helpertext': {
351
+ type: 'string',
352
+ },
353
+ 'ui:bc-helpertext': {
354
+ type: 'string',
355
+ },
356
+ 'ui:bc-required': {
357
+ type: 'boolean',
358
+ },
359
+ 'ui:bc-multiSelect': {
360
+ type: 'boolean',
361
+ },
362
+ 'ui:widget': {
363
+ type: 'string',
364
+ enum: widgetTypes,
365
+ },
366
+ },
367
+ },
368
+
369
+ schemaTypes: {
370
+ string: {
371
+ type: 'object',
372
+ required: ['title'],
373
+ properties: {
374
+ title: {
375
+ type: 'string',
376
+ minLength: 1,
377
+ },
378
+ },
379
+ },
380
+ number: {
381
+ type: 'object',
382
+ required: ['title'],
383
+ properties: {
384
+ title: {
385
+ type: 'string',
386
+ minLength: 1,
387
+ },
388
+ },
389
+ },
390
+ boolean: {
391
+ type: 'object',
392
+ required: ['title'],
393
+ properties: {
394
+ title: {
395
+ type: 'string',
396
+ minLength: 1,
397
+ },
398
+ },
399
+ },
400
+ date: {
401
+ type: 'object',
402
+ required: ['title'],
403
+ properties: {
404
+ title: {
405
+ type: 'string',
406
+ minLength: 1,
407
+ },
408
+ type: {
409
+ const: 'string',
410
+ },
411
+ },
412
+ },
413
+ dateRange: {
414
+ type: 'object',
415
+ required: ['title'],
416
+ properties: {
417
+ title: {
418
+ type: 'string',
419
+ minLength: 1,
420
+ },
421
+ type: {
422
+ const: 'object',
423
+ },
424
+ },
425
+ },
426
+ phone: {
427
+ type: 'object',
428
+ required: ['title'],
429
+ properties: {
430
+ title: {
431
+ type: 'string',
432
+ minLength: 1,
433
+ },
434
+ type: {
435
+ const: 'string',
436
+ },
437
+ },
438
+ },
439
+ skills: {
440
+ type: 'object',
441
+ required: ['title'],
442
+ properties: {
443
+ title: {
444
+ type: 'string',
445
+ minLength: 1,
446
+ },
447
+ uniqueItems: {
448
+ type: 'boolean',
449
+ },
450
+ items: {
451
+ type: 'object',
452
+ properties: {
453
+ anyOf: {
454
+ type: 'array',
455
+ minItems: 1,
456
+ items: {
457
+ type: 'object',
458
+ properties: {
459
+ type: { type: 'string' },
460
+ title: { type: 'string' },
461
+ enum: {
462
+ type: 'array',
463
+ minItems: 1,
464
+ items: {
465
+ type: 'string',
466
+ minLength: 1,
467
+ },
468
+ },
469
+ },
470
+ },
471
+ },
472
+ },
473
+ },
474
+ },
475
+ },
476
+ qualifications: {
477
+ type: 'object',
478
+ required: ['title'],
479
+ properties: {
480
+ title: {
481
+ type: 'string',
482
+ minLength: 1,
483
+ },
484
+ uniqueItems: {
485
+ type: 'boolean',
486
+ },
487
+ items: {
488
+ type: 'object',
489
+ properties: {
490
+ type: {
491
+ const: 'string',
492
+ },
493
+ enum: {
494
+ type: 'array',
495
+ minItems: 1,
496
+ items: [{ type: 'string' }],
497
+ },
498
+ },
499
+ },
500
+ },
501
+ },
502
+ array: {
503
+ type: 'object',
504
+ required: ['title'],
505
+ properties: {
506
+ title: {
507
+ type: 'string',
508
+ minLength: 1,
509
+ },
510
+ uniqueItems: {
511
+ type: 'boolean',
512
+ },
513
+ items: {
514
+ type: 'object',
515
+ properties: {
516
+ anyOf: {
517
+ type: 'array',
518
+ minItems: 1,
519
+ uniqueItems: true,
520
+ items: {
521
+ type: 'object',
522
+ properties: {
523
+ type: { type: 'string' },
524
+ title: { type: 'string' },
525
+ enum: {
526
+ type: 'array',
527
+ minItems: 1,
528
+ items: {
529
+ type: 'string',
530
+ minLength: 1,
531
+ },
532
+ },
533
+ },
534
+ },
535
+ },
536
+ },
537
+ },
538
+ },
539
+ },
540
+ heading: {
541
+ type: 'object',
542
+ required: ['title'],
543
+ properties: {
544
+ title: {
545
+ type: 'string',
546
+ minLength: 1,
547
+ },
548
+ type: {
549
+ const: 'string',
550
+ },
551
+ },
552
+ },
553
+ spacer: {
554
+ type: 'object',
555
+ properties: {
556
+ type: {
557
+ const: 'string',
558
+ },
559
+ },
560
+ },
561
+ location: {
562
+ type: 'object',
563
+ required: ['title'],
564
+ properties: {
565
+ title: {
566
+ type: 'string',
567
+ minLength: 1,
568
+ },
569
+ type: {
570
+ const: 'object',
571
+ },
572
+ },
573
+ },
574
+ file: {
575
+ type: 'object',
576
+ required: ['title'],
577
+ properties: {
578
+ title: {
579
+ type: 'string',
580
+ minLength: 1,
581
+ },
582
+ },
583
+ },
584
+ textParagraph: {
585
+ type: 'object',
586
+ required: ['text'],
587
+ properties: {
588
+ text: {
589
+ type: 'string',
590
+ minLength: 1,
591
+ },
592
+ },
593
+ },
594
+ },
595
+
596
+ // NOTE: schema below is not the whole schema but the individual property
597
+ // eg. { title: 'Phone Number', type: 'string', bcType: 'phone' };
598
+ dataTypeIsValid: {
599
+ string: (data, schema) => {
600
+ const isString =
601
+ schema && schema.required ? !!(typeof data === 'string' && data.length) : !!(typeof data === 'string');
602
+ if (schema && schema.items && schema.items.enum) {
603
+ return isString && schema.items.enum.includes(data);
604
+ }
605
+
606
+ return isString;
607
+ },
608
+ boolean: (data) => {
609
+ return data === true || data === false;
610
+ },
611
+ number: (data) => {
612
+ return typeof data === 'number' && countDecimals(data) <= 10;
613
+ },
614
+ // date and dateRange must be ISOString format
615
+ date: (data) => {
616
+ return typeof data === 'string' && new Date(data).toString() !== 'Invalid Date' && isValidYear(data);
617
+ },
618
+ dateRange: (data) => {
619
+ const fromIsValid = validator.dataTypeIsValid.date(data.from);
620
+ const toIsValid = validator.dataTypeIsValid.date(data.to);
621
+ const toDateIsAFterFromDate = new Date(data.from) <= new Date(data.to);
622
+
623
+ return fromIsValid && toIsValid && toDateIsAFterFromDate;
624
+ },
625
+ phone: (data) => {
626
+ return phoneNumber.isValidNumber(data || '');
627
+ },
628
+ skills: (data, schema) => {
629
+ if (!schema || !schema.items) return false;
630
+
631
+ let hasAnyOf = false;
632
+ try {
633
+ hasAnyOf =
634
+ ajv.validate(
635
+ {
636
+ type: 'array',
637
+ items: schema.items.anyOf,
638
+ minItems: 1,
639
+ },
640
+ data
641
+ ) === true;
642
+ }
643
+ catch (e) {}
644
+ return hasAnyOf;
645
+ },
646
+ qualifications: (data, schema) => {
647
+ if (!schema || !schema.items) return false;
648
+
649
+ return (
650
+ ajv.validate(
651
+ {
652
+ type: 'array',
653
+ items: schema.items,
654
+ minItems: 1,
655
+ },
656
+ data
657
+ ) === true
658
+ );
659
+ },
660
+ array: (data, schema) => {
661
+ if (!schema || !schema.items) return false;
662
+ let hasAnyOf = false;
663
+ let hasItems = false;
664
+ try {
665
+ hasAnyOf =
666
+ ajv.validate(
667
+ {
668
+ type: 'array',
669
+ items: schema.items.anyOf,
670
+ minItems: 1,
671
+ },
672
+ data
673
+ ) === true;
674
+
675
+ hasItems =
676
+ ajv.validate(
677
+ {
678
+ type: 'array',
679
+ items: schema.items,
680
+ minItems: 1,
681
+ },
682
+ data
683
+ ) === true;
684
+ }
685
+ catch (e) {}
686
+
687
+ return hasItems || hasAnyOf;
688
+ },
689
+ heading: () => true,
690
+ spacer: () => true,
691
+ location: (data) => {
692
+ const s = {
693
+ type: 'object',
694
+ required: ['geo'],
695
+ properties: {
696
+ geo: {
697
+ type: 'object',
698
+ required: ['lat', 'lng'],
699
+ properties: {
700
+ lat: {
701
+ type: 'number',
702
+ minimum: -90,
703
+ maximum: 90,
704
+ },
705
+ lng: {
706
+ type: 'number',
707
+ minimum: -180,
708
+ maximum: 180,
709
+ },
710
+ },
711
+ },
712
+ locality: {
713
+ type: 'object',
714
+ properties: {
715
+ long: {
716
+ type: 'string',
717
+ },
718
+ short: {
719
+ type: 'string',
720
+ },
721
+ },
722
+ },
723
+ region: {
724
+ type: 'object',
725
+ properties: {
726
+ long: {
727
+ type: 'string',
728
+ },
729
+ short: {
730
+ type: 'string',
731
+ },
732
+ },
733
+ },
734
+ country: {
735
+ type: 'object',
736
+ properties: {
737
+ long: {
738
+ type: 'string',
739
+ },
740
+ short: {
741
+ type: 'string',
742
+ },
743
+ },
744
+ },
745
+ },
746
+ };
747
+
748
+ if (ajv.validate(s, data) === true) {
749
+ return true;
750
+ }
751
+
752
+ return false;
753
+ },
754
+ file: (data) => {
755
+ if (data === null || typeof data.name === 'string') {
756
+ return true;
757
+ }
758
+
759
+ const s = {
760
+ type: 'object',
761
+ required: ['opportunityId', 'volunteerId', 'filename'],
762
+ properties: {
763
+ opportunityId: {
764
+ type: 'string',
765
+ pattern: '[a-z0-9]{24}',
766
+ },
767
+ volunteerId: {
768
+ type: 'string',
769
+ pattern: '[a-z0-9]{24}',
770
+ },
771
+ filename: {
772
+ type: 'string',
773
+ },
774
+ },
775
+ };
776
+
777
+ if (ajv.validate(s, data) === true) {
778
+ return true;
779
+ }
780
+
781
+ return false;
782
+ },
783
+ },
784
+
785
+ dataTypes: {
786
+ string: (schema, data) => validateType({ type: 'string', data, schema }),
787
+ boolean: (schema, data) => validateType({ type: 'boolean', data }),
788
+ date: (schema, data) => validateType({ type: 'date', data }),
789
+ dateRange: (schema, data) => validateType({ type: 'dateRange', error: 'Property is not a date range', data }),
790
+ phone: (schema, data) => validateType({ type: 'phone', error: 'Property is not a phone number', data }),
791
+ skills: (schema, data) =>
792
+ validateType({ type: 'skills', error: 'Property is not valid list of skills', data, schema }),
793
+ qualifications: (schema, data) =>
794
+ validateType({ type: 'qualifications', error: 'Property is not valid list of qualifications', data, schema }),
795
+ array: (schema, data) =>
796
+ validateType({ type: 'array', error: 'Datatype is not an array of strings', data, schema }),
797
+ heading: (schema, data) => validateType({ type: 'heading', data }),
798
+ spacer: (schema, data) => validateType({ type: 'spacer', data: null }),
799
+ location: (schema, data) =>
800
+ validateType({ type: 'location', error: 'Datatype is not valid location object', data }),
801
+ file: (schema, data) => validateType({ type: 'file', error: 'File must not be set', data }),
802
+ number: (schema, data) => validateType({ type: 'number', error: 'Property is not a number', data }),
803
+ textParagraph: (schema, data) => validateType({ type: 'string', data, schema }),
804
+ },
805
+ };
806
+
807
+ module.exports = validator;