@carecard/validate 3.1.24 → 3.1.25

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.
@@ -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
@@ -161,6 +163,35 @@ function flattenObject(obj, prefix = '', out = {}) {
161
163
  return out;
162
164
  }
163
165
 
166
+ /**
167
+ * Recursively flattens a nested plain object using only each leaf property
168
+ * name as the output key.
169
+ *
170
+ * Example: `{ a: { b: { c: 1, d: 2 } } }` => `{ c: 1, d: 2 }`.
171
+ * If duplicate leaf keys exist at different nesting levels, the higher-level
172
+ * leaf wins. If duplicate leaf keys exist at the same depth, the first
173
+ * traversal wins.
174
+ *
175
+ * @param {Object} obj
176
+ * @param {Object} [out]
177
+ * @param {Object} [depthByKey]
178
+ * @param {number} [depth]
179
+ * @returns {Object}
180
+ */
181
+ function flattenObjectByLeafKey(obj, out = {}, depthByKey = {}, depth = 1) {
182
+ for (const [key, value] of Object.entries(obj)) {
183
+ if (value !== null && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date)) {
184
+ flattenObjectByLeafKey(value, out, depthByKey, depth + 1);
185
+ } else {
186
+ if (!Object.prototype.hasOwnProperty.call(out, key) || depth < depthByKey[key]) {
187
+ out[key] = value;
188
+ depthByKey[key] = depth;
189
+ }
190
+ }
191
+ }
192
+ return out;
193
+ }
194
+
164
195
  /**
165
196
  * Validates and transforms whitelisted properties from an input object.
166
197
  *
@@ -181,8 +212,11 @@ function flattenObject(obj, prefix = '', out = {}) {
181
212
  * element passes validation, and the returned value is an array of the
182
213
  * validated elements (in the same order).
183
214
  * 5. Optionally converts all keys (including nested) to snake_case.
184
- * 6. Optionally flattens the result so every leaf is a top-level key,
185
- * joined by `.` (`flattenOutput`). 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.
186
220
  *
187
221
  * @param {Object} inputObject - The input object (e.g., req.body / req.params).
188
222
  * @param {Array<string>} [requiredProperties=[]] - Leaf paths that MUST be present and valid.
@@ -190,17 +224,26 @@ function flattenObject(obj, prefix = '', out = {}) {
190
224
  * @param {Array<string>} [options.optionalProperties=[]] - Leaf paths allowed but not required.
191
225
  * @param {boolean} [options.convertToSnakeCase=false] - Whether to convert keys to snake_case.
192
226
  * @param {boolean} [options.flattenOutput=false] - Whether to flatten the result so that
193
- * every leaf is a top-level key (joined by `.`), with no nested objects in the output.
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.
194
230
  * @returns {Promise<Object>} Resolves with the validated (and possibly transformed) object.
195
231
  */
196
232
  function validateWhitelistProperties(
197
233
  inputObject,
198
234
  requiredProperties = [],
199
- options = { optionalProperties: [], convertToSnakeCase: false, flattenOutput: false },
235
+ options = { optionalProperties: [], convertToSnakeCase: false, flattenOutput: false, flattenKeyStyle: DEFAULT_FLATTEN_KEY_STYLE },
200
236
  ) {
201
237
  const optionalProperties = (options && options.optionalProperties) || [];
202
238
  const convertToSnakeCase = !!(options && options.convertToSnakeCase);
203
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
+ }
204
247
 
205
248
  // Cap the total number of paths to validate per call.
206
249
  const totalKeys = (requiredProperties ? requiredProperties.length : 0) + optionalProperties.length;
@@ -271,9 +314,9 @@ function validateWhitelistProperties(
271
314
  validatedObject = keysToSnakeCase(validatedObject);
272
315
  }
273
316
 
274
- // 6. Optional flattening: produce a flat object with dot-joined keys.
317
+ // 6. Optional flattening.
275
318
  if (flattenOutput) {
276
- validatedObject = flattenObject(validatedObject);
319
+ validatedObject = flattenKeyStyle === 'leaf' ? flattenObjectByLeafKey(validatedObject) : flattenObject(validatedObject);
277
320
  }
278
321
 
279
322
  return Promise.resolve(validatedObject);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carecard/validate",
3
- "version": "3.1.24",
3
+ "version": "3.1.25",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/CareCard-ca/pkg-validate.git"
@@ -27,18 +27,18 @@
27
27
  "license": "ISC",
28
28
  "devDependencies": {
29
29
  "@types/mocha": "10.0.10",
30
- "@types/node": "25.6.2",
30
+ "@types/node": "25.9.1",
31
31
  "eslint": "9.39.4",
32
32
  "husky": "9.1.7",
33
- "lint-staged": "17.0.3",
34
- "mocha": "11.7.5",
33
+ "lint-staged": "17.0.5",
34
+ "mocha": "11.7.6",
35
35
  "nyc": "18.0.0",
36
36
  "prettier": "3.8.3",
37
37
  "ts-node": "10.9.2",
38
- "typescript": "5.9.3"
38
+ "typescript": "6.0.3"
39
39
  },
40
40
  "dependencies": {
41
- "@carecard/common-util": "^3.1.13"
41
+ "@carecard/common-util": "3.1.15"
42
42
  },
43
43
  "nyc": {
44
44
  "all": true,
package/readme.md CHANGED
@@ -106,6 +106,7 @@ where the package supports both.
106
106
  | `isString6To16CharacterLong` and `isPasswordString` | `strong_password`, `strongPassword` |
107
107
  | `isEmailString` | `email` |
108
108
  | `isPhoneNumber` | `phone_number`, `phoneNumber` |
109
+ | `isCountryCodeString` | `country_code`, `countryCode` |
109
110
  | `isUrlSafeString` | `token`, `email_confirm_token`, `emailConfirmToken`, `verification_token`, `verificationToken` |
110
111
  | `isValidUuidString` | `uuid`, `item_id`, `itemId`, `user_id`, `userId`, `address_id`, `addressId`, `image_id`, `imageId`, `order_id`, `orderId`, `category_id`, `categoryId`, `parent_id`, `parentId`, `college_id`, `collegeId`, `campus_id`, `campusId`, `program_id`, `programId`, `id`, `institution_id`, `institutionId`, `role_assignment_id`, `roleAssignmentId`, `user_role_id`, `userRoleId`, `phone_number_id`, `phoneNumberId`, `entity_id`, `entityId`, `changed_by`, `changedBy`, `request_id`, `requestId` |
111
112
  | `isValidIntegerString` | `offset_number`, `offsetNumber`, `number_of_orders`, `numberOfOrders`, `price`, `from`, `number`, `limit`, `offset` |
@@ -363,3 +364,16 @@ npm run format:check
363
364
 
364
365
  CI runs on Node.js 25 and executes `npm run test:All`. Publishing to npm happens
365
366
  from `main` through the `Publish to npm` GitHub workflow.
367
+
368
+ ## Auth Boundary
369
+
370
+ Validation protects request shape, not authorization. `ms-auth` owns its
371
+ auth-table RLS contract: normal users are self-row only, JWT `roles: ["ad"]`
372
+ is the auth super-admin signal, and public auth flows use narrow system
373
+ contexts. Do not use validators as a replacement for service RLS or database
374
+ context checks.
375
+
376
+ Docs that mention `ms-auth` controller internals should use concise action
377
+ names such as `loginUser`, `registerUser`, `getUserDetail`, and `renewJwt`.
378
+ Access level is conveyed by route middleware and endpoint placement, not by
379
+ `public`/`protected`/`admin`/`Handler` suffixes.