@carecard/validate 3.1.17 → 3.1.18

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.
@@ -18,7 +18,53 @@ const MAX_NESTING_DEPTH = 5;
18
18
  const MAX_KEYS_PER_CALL = 5000;
19
19
 
20
20
  /**
21
- * Splits a dot-notation property path into its segments and validates depth.
21
+ * Returns true if the segment contains a mix of snake_case (underscore) and
22
+ * camelCase (uppercase letter), e.g. `my_mixName`. Such names are ambiguous
23
+ * and are not supported.
24
+ * @param {string} segment
25
+ * @returns {boolean}
26
+ */
27
+ function isMixedCaseSegment(segment) {
28
+ return /_/.test(segment) && /[A-Z]/.test(segment);
29
+ }
30
+
31
+ /**
32
+ * Converts a snake_case identifier to camelCase. Leaves identifiers without
33
+ * underscores unchanged.
34
+ * @param {string} s
35
+ * @returns {string}
36
+ */
37
+ function snakeToCamel(s) {
38
+ return s.replace(/_([a-zA-Z0-9])/g, (_, c) => c.toUpperCase());
39
+ }
40
+
41
+ /**
42
+ * Converts a camelCase identifier to snake_case. Leaves identifiers without
43
+ * uppercase letters unchanged.
44
+ * @param {string} s
45
+ * @returns {string}
46
+ */
47
+ function camelToSnake(s) {
48
+ return s.replace(/[A-Z]/g, c => `_${c.toLowerCase()}`);
49
+ }
50
+
51
+ /**
52
+ * Returns the alternate-case form of the segment:
53
+ * - snake_case -> camelCase
54
+ * - camelCase -> snake_case
55
+ * - otherwise (no `_` and no uppercase) the segment itself.
56
+ * @param {string} segment
57
+ * @returns {string}
58
+ */
59
+ function alternateCase(segment) {
60
+ if (segment.indexOf('_') !== -1) return snakeToCamel(segment);
61
+ if (/[A-Z]/.test(segment)) return camelToSnake(segment);
62
+ return segment;
63
+ }
64
+
65
+ /**
66
+ * Splits a dot-notation property path into its segments and validates depth
67
+ * and per-segment casing (mixed snake/camel segments are rejected).
22
68
  * @param {string} path
23
69
  * @returns {string[]}
24
70
  */
@@ -29,6 +75,13 @@ function splitPath(path) {
29
75
  userMessage: `Property path "${path}" exceeds maximum nesting depth of ${MAX_NESTING_DEPTH}`,
30
76
  });
31
77
  }
78
+ for (const seg of segments) {
79
+ if (isMixedCaseSegment(seg)) {
80
+ throwBadInputError({
81
+ userMessage: `Property path "${path}" has a segment "${seg}" mixing snake_case and camelCase`,
82
+ });
83
+ }
84
+ }
32
85
  return segments;
33
86
  }
34
87
 
@@ -42,16 +95,27 @@ function splitPath(path) {
42
95
  * @returns {{ found: boolean, value: any }}
43
96
  */
44
97
  function readLeaf(obj, segments) {
98
+ // Resolve a segment against the current node, trying its as-written form
99
+ // first and then its alternate snake/camel form. Returns the actual key
100
+ // present in the node, or undefined if neither form exists.
101
+ function resolveKey(node, seg) {
102
+ if (Object.prototype.hasOwnProperty.call(node, seg)) return seg;
103
+ const alt = alternateCase(seg);
104
+ if (alt !== seg && Object.prototype.hasOwnProperty.call(node, alt)) return alt;
105
+ return undefined;
106
+ }
107
+
45
108
  let current = obj;
46
109
  for (let i = 0; i < segments.length - 1; i++) {
47
110
  if (current === null || typeof current !== 'object') return { found: false, value: undefined };
48
- if (!Object.prototype.hasOwnProperty.call(current, segments[i])) return { found: false, value: undefined };
49
- current = current[segments[i]];
111
+ const key = resolveKey(current, segments[i]);
112
+ if (key === undefined) return { found: false, value: undefined };
113
+ current = current[key];
50
114
  }
51
115
  if (current === null || typeof current !== 'object') return { found: false, value: undefined };
52
- const leaf = segments[segments.length - 1];
53
- if (!Object.prototype.hasOwnProperty.call(current, leaf)) return { found: false, value: undefined };
54
- return { found: true, value: current[leaf] };
116
+ const leafKey = resolveKey(current, segments[segments.length - 1]);
117
+ if (leafKey === undefined) return { found: false, value: undefined };
118
+ return { found: true, value: current[leafKey] };
55
119
  }
56
120
 
57
121
  /**
@@ -146,7 +210,7 @@ function validateWhitelistProperties(
146
210
  });
147
211
  }
148
212
 
149
- const requiredPaths = requiredProperties.map(p => ({ raw: p, segments: splitPath(p) }));
213
+ const requiredPaths = (requiredProperties || []).map(p => ({ raw: p, segments: splitPath(p) }));
150
214
  const optionalPaths = optionalProperties.map(p => ({ raw: p, segments: splitPath(p) }));
151
215
 
152
216
  let validatedObject = {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carecard/validate",
3
- "version": "3.1.17",
3
+ "version": "3.1.18",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/CareCard-ca/pkg-validate.git"