@carecard/validate 3.19.0 → 3.20.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.
- package/lib/validate.js +40 -9
- package/lib/validateNewUserRoleRequest.js +30 -12
- package/package.json +2 -2
package/lib/validate.js
CHANGED
|
@@ -256,24 +256,59 @@ const isValidDomainName = domain => {
|
|
|
256
256
|
return domainRegex.test(domain);
|
|
257
257
|
};
|
|
258
258
|
|
|
259
|
+
// Pattern: Pure Function - validates calendar and clock fields without Date.parse normalization.
|
|
260
|
+
const hasValidTimestampFields = match => {
|
|
261
|
+
if (!match) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
const [, year, month, day, hour, minute, second] = match;
|
|
265
|
+
return (
|
|
266
|
+
isValidCalendarDate(Number(year), Number(month), Number(day)) &&
|
|
267
|
+
Number(hour) <= 23 &&
|
|
268
|
+
Number(minute) <= 59 &&
|
|
269
|
+
Number(second) <= 59
|
|
270
|
+
);
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// Pattern: Pure Function - validates the numeric UTC offset carried by a timestamp.
|
|
274
|
+
const hasValidTimestampOffset = match => {
|
|
275
|
+
const zone = match?.[7];
|
|
276
|
+
return zone === 'Z' || (Number(match?.[8]) <= 23 && Number(match?.[9]) <= 59);
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
// Pattern: Predicate - accepts only real ISO timestamps with an explicit time zone.
|
|
259
280
|
const isValidTimestampzString = str => {
|
|
260
281
|
if (typeof str !== 'string' || str.length === 0 || str.length > 64) {
|
|
261
282
|
return false;
|
|
262
283
|
}
|
|
263
284
|
// ISO 8601 for UTC or with Offset: 2023-10-27T10:00:00Z or 2023-10-27T10:00:00+02:00
|
|
264
|
-
const timestampzRegex =
|
|
265
|
-
|
|
285
|
+
const timestampzRegex =
|
|
286
|
+
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(Z|[+-](\d{2}):(\d{2}))$/;
|
|
287
|
+
const match = str.match(timestampzRegex);
|
|
288
|
+
return hasValidTimestampFields(match) && hasValidTimestampOffset(match);
|
|
266
289
|
};
|
|
267
290
|
|
|
291
|
+
// Pattern: Predicate - accepts only real ISO local timestamps without a time zone.
|
|
268
292
|
const isValidTimestampString = str => {
|
|
269
293
|
if (typeof str !== 'string' || str.length === 0 || str.length > 64) {
|
|
270
294
|
return false;
|
|
271
295
|
}
|
|
272
296
|
// ISO 8601 without time zone: 2023-10-27T10:00:00 or 2023-10-27T10:00:00.123
|
|
273
|
-
const timestampRegex =
|
|
274
|
-
return
|
|
297
|
+
const timestampRegex = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?$/;
|
|
298
|
+
return hasValidTimestampFields(str.match(timestampRegex));
|
|
275
299
|
};
|
|
276
300
|
|
|
301
|
+
// Pattern: Pure Function - validates Gregorian date fields without runtime date coercion.
|
|
302
|
+
function isValidCalendarDate(year, month, day) {
|
|
303
|
+
if (year < 1 || month < 1 || month > 12 || day < 1) {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
const leapYear = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
307
|
+
const monthLengths = [31, leapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
308
|
+
return day <= monthLengths[month - 1];
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Pattern: Predicate - accepts only real Gregorian dates in YYYY-MM-DD form.
|
|
277
312
|
const isValidDateString = str => {
|
|
278
313
|
if (typeof str !== 'string' || str.length === 0 || str.length > 10) {
|
|
279
314
|
return false;
|
|
@@ -288,11 +323,7 @@ const isValidDateString = str => {
|
|
|
288
323
|
const year = Number(match[1]);
|
|
289
324
|
const month = Number(match[2]);
|
|
290
325
|
const day = Number(match[3]);
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
return (
|
|
294
|
-
date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day
|
|
295
|
-
);
|
|
326
|
+
return isValidCalendarDate(year, month, day);
|
|
296
327
|
};
|
|
297
328
|
|
|
298
329
|
const isValidUrl = url => {
|
|
@@ -8,25 +8,16 @@ const { isUserRoleRequestRoleString } = require('./validate');
|
|
|
8
8
|
const DEFAULT_USER_ROLE_REQUEST_ROLE = 'student';
|
|
9
9
|
const REQUIRE_SCOPE_WHEN_ROLE_OR_SCOPE_PRESENT = 'whenRoleOrScopePresent';
|
|
10
10
|
|
|
11
|
+
// Pattern: Boundary Normalizer - returns canonical persisted fields for supported caller shapes.
|
|
11
12
|
function validateNewUserRoleRequestObject(roleRequest = {}, options = {}) {
|
|
12
13
|
const normalized = normalizeNewUserRoleRequestObject(roleRequest);
|
|
13
|
-
const defaultRole =
|
|
14
|
-
? options.defaultRole
|
|
15
|
-
: DEFAULT_USER_ROLE_REQUEST_ROLE;
|
|
16
|
-
const requireScope = Object.prototype.hasOwnProperty.call(options, 'requireScope')
|
|
17
|
-
? options.requireScope
|
|
18
|
-
: true;
|
|
14
|
+
const { defaultRole, requireScope } = readNewUserRoleRequestOptions(options);
|
|
19
15
|
|
|
20
16
|
if (normalized.role_name === undefined && defaultRole !== undefined) {
|
|
21
17
|
normalized.role_name = defaultRole;
|
|
22
18
|
}
|
|
23
19
|
|
|
24
|
-
|
|
25
|
-
throwBadInputError({
|
|
26
|
-
userMessage: 'Invalid property: role.role',
|
|
27
|
-
details: { role: 'Role requests are limited to student, intern, or volunteer' },
|
|
28
|
-
});
|
|
29
|
-
}
|
|
20
|
+
normalizeNewUserRoleRequestRole(normalized);
|
|
30
21
|
|
|
31
22
|
if (shouldRequireScope(normalized, requireScope)) {
|
|
32
23
|
requireNewUserRoleRequestScope(normalized);
|
|
@@ -35,6 +26,33 @@ function validateNewUserRoleRequestObject(roleRequest = {}, options = {}) {
|
|
|
35
26
|
return normalized;
|
|
36
27
|
}
|
|
37
28
|
|
|
29
|
+
// Pattern: Options Object - supplies stable defaults for untyped JavaScript callers.
|
|
30
|
+
function readNewUserRoleRequestOptions(options) {
|
|
31
|
+
const validatedOptions = options && typeof options === 'object' ? options : {};
|
|
32
|
+
return {
|
|
33
|
+
defaultRole: Object.hasOwn(validatedOptions, 'defaultRole')
|
|
34
|
+
? validatedOptions.defaultRole
|
|
35
|
+
: DEFAULT_USER_ROLE_REQUEST_ROLE,
|
|
36
|
+
requireScope: Object.hasOwn(validatedOptions, 'requireScope')
|
|
37
|
+
? validatedOptions.requireScope
|
|
38
|
+
: true,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Pattern: Canonicalization - validates and normalizes the persisted role enum in one boundary.
|
|
43
|
+
function normalizeNewUserRoleRequestRole(roleRequest) {
|
|
44
|
+
if (roleRequest.role_name === undefined) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (!isUserRoleRequestRoleString(roleRequest.role_name)) {
|
|
48
|
+
throwBadInputError({
|
|
49
|
+
userMessage: 'Invalid property: role.role',
|
|
50
|
+
details: { role: 'Role requests are limited to student, intern, or volunteer' },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
roleRequest.role_name = roleRequest.role_name.trim().toLowerCase();
|
|
54
|
+
}
|
|
55
|
+
|
|
38
56
|
function normalizeNewUserRoleRequestObject(roleRequest) {
|
|
39
57
|
const normalized = { ...roleRequest };
|
|
40
58
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carecard/validate",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.20.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/CareCard-ca/pkg-validate.git"
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"typescript": "6.0.3"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@carecard/common-util": "3.
|
|
47
|
+
"@carecard/common-util": "3.20.0"
|
|
48
48
|
},
|
|
49
49
|
"nyc": {
|
|
50
50
|
"all": true,
|