@carecard/validate 3.1.20 → 3.1.21

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/index.d.ts CHANGED
@@ -13,13 +13,18 @@ export interface ValidateWhitelistPropertiesOptions {
13
13
  convertToSnakeCase?: boolean;
14
14
  /**
15
15
  * When true, the returned object is flattened so that every validated leaf
16
- * becomes a top-level key. Existing dot-path flattening is preserved, and
17
- * multiple leaves from the same nested parent can flatten to direct leaf
18
- * property names (e.g. `{ email: 'Jane' }`). If duplicate direct leaf keys
19
- * exist at different nesting levels, the higher-level property wins. No
20
- * nested objects remain in the output. Applied after snake_case conversion.
16
+ * becomes a top-level key. No nested objects remain in the output. Applied
17
+ * after snake_case conversion.
21
18
  */
22
19
  flattenOutput?: boolean;
20
+ /**
21
+ * Controls flattened key naming when `flattenOutput` is true.
22
+ * - `path` uses full dot-notation paths, e.g. `{ "user.email": "Jane" }`.
23
+ * - `leaf` uses only leaf names, e.g. `{ email: "Jane" }`.
24
+ *
25
+ * Defaults to `path`.
26
+ */
27
+ flattenKeyStyle?: 'path' | 'leaf';
23
28
  }
24
29
 
25
30
  /**
@@ -16,6 +16,8 @@ const MAX_NESTING_DEPTH = 5;
16
16
  * adversarial inputs.
17
17
  */
18
18
  const MAX_KEYS_PER_CALL = 5000;
19
+ const DEFAULT_FLATTEN_KEY_STYLE = 'path';
20
+ const VALID_FLATTEN_KEY_STYLES = new Set(['path', 'leaf']);
19
21
 
20
22
  /**
21
23
  * Returns true if the segment contains a mix of snake_case (underscore) and
@@ -190,51 +192,6 @@ function flattenObjectByLeafKey(obj, out = {}, depthByKey = {}, depth = 1) {
190
192
  return out;
191
193
  }
192
194
 
193
- /**
194
- * Detects whether the flattened leaf-key output would contain duplicate keys
195
- * from different nesting depths.
196
- *
197
- * @param {Object} obj
198
- * @param {Object} [depthByKey]
199
- * @param {number} [depth]
200
- * @returns {boolean}
201
- */
202
- function hasDuplicateLeafKeyAtDifferentDepth(obj, depthByKey = {}, depth = 1) {
203
- for (const [key, value] of Object.entries(obj)) {
204
- if (value !== null && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date)) {
205
- if (hasDuplicateLeafKeyAtDifferentDepth(value, depthByKey, depth + 1)) {
206
- return true;
207
- }
208
- } else if (Object.prototype.hasOwnProperty.call(depthByKey, key)) {
209
- if (depthByKey[key] !== depth) {
210
- return true;
211
- }
212
- } else {
213
- depthByKey[key] = depth;
214
- }
215
- }
216
- return false;
217
- }
218
-
219
- /**
220
- * Uses leaf-key flattening only when multiple validated leaves came from the
221
- * same nested parent, or when duplicate leaf keys exist at different nesting
222
- * depths and require the higher-level key to win. This preserves the existing
223
- * dot-path flatten output for current callers while supporting the sibling-leaf
224
- * output shape.
225
- *
226
- * @param {Array<string[]>} validatedSegments
227
- * @param {Object} validatedObject
228
- * @returns {boolean}
229
- */
230
- function shouldFlattenByLeafKey(validatedSegments, validatedObject) {
231
- if (hasDuplicateLeafKeyAtDifferentDepth(validatedObject)) return true;
232
- if (validatedSegments.length < 2) return false;
233
- const firstParent = validatedSegments[0].slice(0, -1).join('.');
234
- if (!firstParent || validatedSegments[0].length <= 2) return false;
235
- return validatedSegments.every(segments => segments.slice(0, -1).join('.') === firstParent);
236
- }
237
-
238
195
  /**
239
196
  * Validates and transforms whitelisted properties from an input object.
240
197
  *
@@ -255,11 +212,11 @@ function shouldFlattenByLeafKey(validatedSegments, validatedObject) {
255
212
  * element passes validation, and the returned value is an array of the
256
213
  * validated elements (in the same order).
257
214
  * 5. Optionally converts all keys (including nested) to snake_case.
258
- * 6. Optionally flattens the result (`flattenOutput`). Existing dot-path
259
- * flattening is preserved, while multiple leaves from the same nested
260
- * parent, or duplicate leaf keys at different nesting depths, flatten to
261
- * direct leaf keys. For duplicates at different depths, the higher-level
262
- * property wins. Applied after snake_case conversion.
215
+ * 6. Optionally flattens the result (`flattenOutput`). Flattened keys use
216
+ * full dot paths by default (`flattenKeyStyle: 'path'`) or direct leaf
217
+ * names when requested (`flattenKeyStyle: 'leaf'`). For duplicate leaf
218
+ * keys in leaf mode, the shallower value wins; ties keep the first value
219
+ * encountered. Applied after snake_case conversion.
263
220
  *
264
221
  * @param {Object} inputObject - The input object (e.g., req.body / req.params).
265
222
  * @param {Array<string>} [requiredProperties=[]] - Leaf paths that MUST be present and valid.
@@ -268,16 +225,25 @@ function shouldFlattenByLeafKey(validatedSegments, validatedObject) {
268
225
  * @param {boolean} [options.convertToSnakeCase=false] - Whether to convert keys to snake_case.
269
226
  * @param {boolean} [options.flattenOutput=false] - Whether to flatten the result so that
270
227
  * every leaf is a top-level key, with no nested objects in the output.
228
+ * @param {'path'|'leaf'} [options.flattenKeyStyle='path'] - Flattened key naming strategy
229
+ * when `flattenOutput` is true. `path` uses dot-joined paths; `leaf` uses leaf names.
271
230
  * @returns {Promise<Object>} Resolves with the validated (and possibly transformed) object.
272
231
  */
273
232
  function validateWhitelistProperties(
274
233
  inputObject,
275
234
  requiredProperties = [],
276
- options = { optionalProperties: [], convertToSnakeCase: false, flattenOutput: false },
235
+ options = { optionalProperties: [], convertToSnakeCase: false, flattenOutput: false, flattenKeyStyle: DEFAULT_FLATTEN_KEY_STYLE },
277
236
  ) {
278
237
  const optionalProperties = (options && options.optionalProperties) || [];
279
238
  const convertToSnakeCase = !!(options && options.convertToSnakeCase);
280
239
  const flattenOutput = !!(options && options.flattenOutput);
240
+ const flattenKeyStyle = options && options.flattenKeyStyle !== undefined ? options.flattenKeyStyle : DEFAULT_FLATTEN_KEY_STYLE;
241
+
242
+ if (!VALID_FLATTEN_KEY_STYLES.has(flattenKeyStyle)) {
243
+ throwBadInputError({
244
+ userMessage: `Invalid flattenKeyStyle: ${String(flattenKeyStyle)}. Expected "path" or "leaf"`,
245
+ });
246
+ }
281
247
 
282
248
  // Cap the total number of paths to validate per call.
283
249
  const totalKeys = (requiredProperties ? requiredProperties.length : 0) + optionalProperties.length;
@@ -291,7 +257,6 @@ function validateWhitelistProperties(
291
257
  const optionalPaths = optionalProperties.map(p => ({ raw: p, segments: splitPath(p) }));
292
258
 
293
259
  let validatedObject = {};
294
- const validatedSegments = [];
295
260
 
296
261
  // Helper: validate a single leaf value by feeding `{ [leafKey]: value }` to
297
262
  // `validateProperties` and checking whether the leaf key survived.
@@ -330,7 +295,6 @@ function validateWhitelistProperties(
330
295
  throwBadInputError({ userMessage: `Missing or invalid property: ${raw}` });
331
296
  }
332
297
  writeLeaf(validatedObject, segments, validatedValue);
333
- validatedSegments.push(segments);
334
298
  });
335
299
 
336
300
  // 1 + 4. Optional paths: if provided, must be valid.
@@ -343,7 +307,6 @@ function validateWhitelistProperties(
343
307
  throwBadInputError({ userMessage: `Invalid property value: ${raw}` });
344
308
  }
345
309
  writeLeaf(validatedObject, segments, validatedValue);
346
- validatedSegments.push(segments);
347
310
  });
348
311
 
349
312
  // 5. Optional case transformation (recursive, handles nested keys).
@@ -353,9 +316,7 @@ function validateWhitelistProperties(
353
316
 
354
317
  // 6. Optional flattening.
355
318
  if (flattenOutput) {
356
- validatedObject = shouldFlattenByLeafKey(validatedSegments, validatedObject)
357
- ? flattenObjectByLeafKey(validatedObject)
358
- : flattenObject(validatedObject);
319
+ validatedObject = flattenKeyStyle === 'leaf' ? flattenObjectByLeafKey(validatedObject) : flattenObject(validatedObject);
359
320
  }
360
321
 
361
322
  return Promise.resolve(validatedObject);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carecard/validate",
3
- "version": "3.1.20",
3
+ "version": "3.1.21",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/CareCard-ca/pkg-validate.git"
package/readme.md CHANGED
@@ -139,13 +139,63 @@ const out = await validateWhitelistProperties(body, ['first_name', 'email'], {
139
139
  // }
140
140
  ```
141
141
 
142
+ ### Defaults
143
+
144
+ When omitted, `requiredProperties` defaults to `[]` and `options` defaults to:
145
+
146
+ ```js
147
+ {
148
+ optionalProperties: [],
149
+ convertToSnakeCase: false,
150
+ flattenOutput: false,
151
+ flattenKeyStyle: 'path',
152
+ }
153
+ ```
154
+
155
+ The default output preserves the nested shape described by whitelisted dot paths.
156
+ `flattenKeyStyle` only changes output when `flattenOutput` is `true`.
157
+
158
+ ```js
159
+ const input = {
160
+ user: {
161
+ first_name: 'Jane',
162
+ contact: { email: 'jane@example.com' },
163
+ },
164
+ };
165
+
166
+ await validateWhitelistProperties(input, ['user.first_name', 'user.contact.email']);
167
+ // {
168
+ // user: {
169
+ // first_name: 'Jane',
170
+ // contact: { email: 'jane@example.com' }
171
+ // }
172
+ // }
173
+ ```
174
+
142
175
  ### Options
143
176
 
144
- | Option | Default | Behavior |
145
- | -------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
146
- | `optionalProperties` | `[]` | Additional property paths that may be present. If present, each value must be valid. |
147
- | `convertToSnakeCase` | `false` | Converts returned keys, including nested keys, to snake_case using `@carecard/common-util`. |
148
- | `flattenOutput` | `false` | Flattens returned nested objects. Existing dot-path output is preserved, and sibling leaves from the same nested parent can flatten to direct leaf keys. If duplicate direct leaf keys occur at different nesting levels, the higher-level property wins. Applied after snake_case conversion. |
177
+ | Option | Default | Behavior |
178
+ | -------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
179
+ | `optionalProperties` | `[]` | Additional property paths that may be present. Absent optional paths are ignored. Present optional paths must be valid. |
180
+ | `convertToSnakeCase` | `false` | When `true`, converts returned keys, including nested keys, to snake_case using `@carecard/common-util`. Conversion happens before flattening. |
181
+ | `flattenOutput` | `false` | When `true`, removes nested objects from the returned value so every validated leaf becomes a top-level key. |
182
+ | `flattenKeyStyle` | `'path'` | Controls flattened key naming when `flattenOutput` is `true`. Use `'path'` for full dot-notation keys or `'leaf'` for direct leaf names. Invalid values throw a `BAD_INPUT` error. |
183
+
184
+ Example with only the default options:
185
+
186
+ ```js
187
+ await validateWhitelistProperties({ first_name: 'Jane', email: 'jane@example.com', ignored: 'x' }, ['first_name']);
188
+ // { first_name: 'Jane' }
189
+ ```
190
+
191
+ Example with optional properties:
192
+
193
+ ```js
194
+ await validateWhitelistProperties({ first_name: 'Jane', phone_number: '4165551234' }, ['first_name'], {
195
+ optionalProperties: ['phone_number'],
196
+ });
197
+ // { first_name: 'Jane', phone_number: '4165551234' }
198
+ ```
149
199
 
150
200
  ### Required And Optional Values
151
201
 
@@ -236,20 +286,31 @@ await validateWhitelistProperties(input, ['a.b.c.d.email']);
236
286
  // { a: { b: { c: { d: { email: 'jane@example.com' } } } } }
237
287
  ```
238
288
 
239
- With `flattenOutput: true`, sibling leaves from the same nested parent are
240
- returned as top-level leaf keys:
289
+ With `flattenOutput: true`, keys are full dot paths by default:
290
+
291
+ ```js
292
+ const input = { a: { b: { c: { d: { email: 'jane@example.com', name: 'Jane' } } } } };
293
+ await validateWhitelistProperties(input, ['a.b.c.d.email', 'a.b.c.d.name'], {
294
+ flattenOutput: true,
295
+ });
296
+ // { 'a.b.c.d.email': 'jane@example.com', 'a.b.c.d.name': 'Jane' }
297
+ ```
298
+
299
+ Use `flattenKeyStyle: 'leaf'` to return top-level leaf keys instead:
241
300
 
242
301
  ```js
243
302
  const input = { a: { b: { c: { d: { email: 'jane@example.com', name: 'Jane' } } } } };
244
303
  await validateWhitelistProperties(input, ['a.b.c.d.email', 'a.b.c.d.name'], {
245
304
  flattenOutput: true,
305
+ flattenKeyStyle: 'leaf',
246
306
  });
247
307
  // { email: 'jane@example.com', name: 'Jane' }
248
308
  ```
249
309
 
250
- If direct leaf-key flattening produces duplicate keys at different nesting
310
+ When `flattenKeyStyle: 'leaf'` produces duplicate keys at different nesting
251
311
  levels, the higher-level value is kept and the lower-level duplicate is
252
- discarded:
312
+ discarded. Duplicate leaf keys at the same nesting depth keep the first value
313
+ encountered:
253
314
 
254
315
  ```js
255
316
  const input = {
@@ -259,6 +320,7 @@ const input = {
259
320
 
260
321
  await validateWhitelistProperties(input, ['name', 'user.name', 'user.email'], {
261
322
  flattenOutput: true,
323
+ flattenKeyStyle: 'leaf',
262
324
  });
263
325
  // { name: 'Top Level Name', email: 'jane@example.com' }
264
326
  ```