@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 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 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
265
- return timestampzRegex.test(str) && !isNaN(Date.parse(str));
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 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?$/;
274
- return timestampRegex.test(str) && !isNaN(Date.parse(str));
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
- const date = new Date(Date.UTC(year, month - 1, day));
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 = Object.prototype.hasOwnProperty.call(options, '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
- if (normalized.role_name !== undefined && !isUserRoleRequestRoleString(normalized.role_name)) {
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.19.0",
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.18.0"
47
+ "@carecard/common-util": "3.20.0"
48
48
  },
49
49
  "nyc": {
50
50
  "all": true,