@carecard/validate 3.1.19 → 3.1.20
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 +7 -3
- package/lib/validateWhitelistProperties.js +87 -5
- package/package.json +1 -1
- package/readme.md +283 -11
package/index.d.ts
CHANGED
|
@@ -13,8 +13,11 @@ 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
|
|
17
|
-
*
|
|
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.
|
|
18
21
|
*/
|
|
19
22
|
flattenOutput?: boolean;
|
|
20
23
|
}
|
|
@@ -38,10 +41,11 @@ export interface ValidateWhitelistPropertiesOptions {
|
|
|
38
41
|
* validated elements (e.g. `{ name: ["First", "Other"] }` is validated like
|
|
39
42
|
* `{ name: "First" }` and `{ name: "Other" }` individually).
|
|
40
43
|
* - Optionally converts the resulting keys (including nested keys) to snake_case.
|
|
44
|
+
* - Optionally flattens the result after snake_case conversion.
|
|
41
45
|
*
|
|
42
46
|
* @param inputObject The input object (e.g. `req.body` or `req.params`).
|
|
43
47
|
* @param requiredProperties Leaf paths that must be present and valid. Dot-notation supported.
|
|
44
|
-
* @param options Optional
|
|
48
|
+
* @param options Optional additional leaf paths plus output transformation flags.
|
|
45
49
|
*/
|
|
46
50
|
export function validateWhitelistProperties(
|
|
47
51
|
inputObject: Record<string, any>,
|
|
@@ -161,6 +161,80 @@ function flattenObject(obj, prefix = '', out = {}) {
|
|
|
161
161
|
return out;
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Recursively flattens a nested plain object using only each leaf property
|
|
166
|
+
* name as the output key.
|
|
167
|
+
*
|
|
168
|
+
* Example: `{ a: { b: { c: 1, d: 2 } } }` => `{ c: 1, d: 2 }`.
|
|
169
|
+
* If duplicate leaf keys exist at different nesting levels, the higher-level
|
|
170
|
+
* leaf wins. If duplicate leaf keys exist at the same depth, the first
|
|
171
|
+
* traversal wins.
|
|
172
|
+
*
|
|
173
|
+
* @param {Object} obj
|
|
174
|
+
* @param {Object} [out]
|
|
175
|
+
* @param {Object} [depthByKey]
|
|
176
|
+
* @param {number} [depth]
|
|
177
|
+
* @returns {Object}
|
|
178
|
+
*/
|
|
179
|
+
function flattenObjectByLeafKey(obj, out = {}, depthByKey = {}, depth = 1) {
|
|
180
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
181
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date)) {
|
|
182
|
+
flattenObjectByLeafKey(value, out, depthByKey, depth + 1);
|
|
183
|
+
} else {
|
|
184
|
+
if (!Object.prototype.hasOwnProperty.call(out, key) || depth < depthByKey[key]) {
|
|
185
|
+
out[key] = value;
|
|
186
|
+
depthByKey[key] = depth;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
192
|
+
|
|
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
|
+
|
|
164
238
|
/**
|
|
165
239
|
* Validates and transforms whitelisted properties from an input object.
|
|
166
240
|
*
|
|
@@ -181,8 +255,11 @@ function flattenObject(obj, prefix = '', out = {}) {
|
|
|
181
255
|
* element passes validation, and the returned value is an array of the
|
|
182
256
|
* validated elements (in the same order).
|
|
183
257
|
* 5. Optionally converts all keys (including nested) to snake_case.
|
|
184
|
-
* 6. Optionally flattens the result
|
|
185
|
-
*
|
|
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.
|
|
186
263
|
*
|
|
187
264
|
* @param {Object} inputObject - The input object (e.g., req.body / req.params).
|
|
188
265
|
* @param {Array<string>} [requiredProperties=[]] - Leaf paths that MUST be present and valid.
|
|
@@ -190,7 +267,7 @@ function flattenObject(obj, prefix = '', out = {}) {
|
|
|
190
267
|
* @param {Array<string>} [options.optionalProperties=[]] - Leaf paths allowed but not required.
|
|
191
268
|
* @param {boolean} [options.convertToSnakeCase=false] - Whether to convert keys to snake_case.
|
|
192
269
|
* @param {boolean} [options.flattenOutput=false] - Whether to flatten the result so that
|
|
193
|
-
* every leaf is a top-level key
|
|
270
|
+
* every leaf is a top-level key, with no nested objects in the output.
|
|
194
271
|
* @returns {Promise<Object>} Resolves with the validated (and possibly transformed) object.
|
|
195
272
|
*/
|
|
196
273
|
function validateWhitelistProperties(
|
|
@@ -214,6 +291,7 @@ function validateWhitelistProperties(
|
|
|
214
291
|
const optionalPaths = optionalProperties.map(p => ({ raw: p, segments: splitPath(p) }));
|
|
215
292
|
|
|
216
293
|
let validatedObject = {};
|
|
294
|
+
const validatedSegments = [];
|
|
217
295
|
|
|
218
296
|
// Helper: validate a single leaf value by feeding `{ [leafKey]: value }` to
|
|
219
297
|
// `validateProperties` and checking whether the leaf key survived.
|
|
@@ -252,6 +330,7 @@ function validateWhitelistProperties(
|
|
|
252
330
|
throwBadInputError({ userMessage: `Missing or invalid property: ${raw}` });
|
|
253
331
|
}
|
|
254
332
|
writeLeaf(validatedObject, segments, validatedValue);
|
|
333
|
+
validatedSegments.push(segments);
|
|
255
334
|
});
|
|
256
335
|
|
|
257
336
|
// 1 + 4. Optional paths: if provided, must be valid.
|
|
@@ -264,6 +343,7 @@ function validateWhitelistProperties(
|
|
|
264
343
|
throwBadInputError({ userMessage: `Invalid property value: ${raw}` });
|
|
265
344
|
}
|
|
266
345
|
writeLeaf(validatedObject, segments, validatedValue);
|
|
346
|
+
validatedSegments.push(segments);
|
|
267
347
|
});
|
|
268
348
|
|
|
269
349
|
// 5. Optional case transformation (recursive, handles nested keys).
|
|
@@ -271,9 +351,11 @@ function validateWhitelistProperties(
|
|
|
271
351
|
validatedObject = keysToSnakeCase(validatedObject);
|
|
272
352
|
}
|
|
273
353
|
|
|
274
|
-
// 6. Optional flattening
|
|
354
|
+
// 6. Optional flattening.
|
|
275
355
|
if (flattenOutput) {
|
|
276
|
-
validatedObject =
|
|
356
|
+
validatedObject = shouldFlattenByLeafKey(validatedSegments, validatedObject)
|
|
357
|
+
? flattenObjectByLeafKey(validatedObject)
|
|
358
|
+
: flattenObject(validatedObject);
|
|
277
359
|
}
|
|
278
360
|
|
|
279
361
|
return Promise.resolve(validatedObject);
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -1,29 +1,301 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @carecard/validate
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`@carecard/validate` is a small CommonJS validation package for CareCard
|
|
4
|
+
services. It exposes individual value validators, a bulk property sanitizer, and
|
|
5
|
+
a whitelist validator for request-like payloads.
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
The package returns booleans from the low-level validators, strips unknown or
|
|
8
|
+
invalid values from `validateProperties`, and throws CareCard `BAD_INPUT` errors
|
|
9
|
+
from `validateWhitelistProperties` when required or provided whitelist values do
|
|
10
|
+
not pass validation.
|
|
6
11
|
|
|
7
|
-
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm install @carecard/validate
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Importing
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
const { validate, validateProperties, validateWhitelistProperties, isEmailString, isValidUuidString } = require('@carecard/validate');
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The validators are available both as top-level exports and under the deprecated
|
|
25
|
+
`validate` namespace.
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
isEmailString('jane@example.com'); // true
|
|
29
|
+
validate.isEmailString('jane@example.com'); // true
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Direct Validators
|
|
33
|
+
|
|
34
|
+
Every direct validator returns `true` or `false`. Failure message helpers return
|
|
35
|
+
a string on failure and `null` on success.
|
|
36
|
+
|
|
37
|
+
| Function | Accepted value |
|
|
38
|
+
| --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
39
|
+
| `isImageUrl(value)` | Non-empty string up to 2048 chars using letters, numbers, `-`, `_`, `.`, and `/`. Intended for safe image/file paths. |
|
|
40
|
+
| `isInteger(value)` | JavaScript integer number. String numbers are rejected. |
|
|
41
|
+
| `isValidJsonString(value)` | Non-empty string up to 10000 chars that parses to a non-null JSON object or array. JSON primitives are rejected. |
|
|
42
|
+
| `isValidIntegerString(value)` | Digit-only string, 1 to 20 chars. No signs or decimals. |
|
|
43
|
+
| `isValidUuidString(value)` | Canonical UUID string in `8-4-4-4-12` format, case-insensitive. |
|
|
44
|
+
| `isCharactersString(value)` | 1 to 1000 chars containing letters, numbers, spaces, `_`, or `-`. |
|
|
45
|
+
| `isNameString(value)` | 1 to 1000 char string that starts with a letter and uses letters, numbers, spaces, `_`, `-`, `.`, `,`, `'`, `(`, or `)`. Leading/trailing spaces are trimmed before pattern validation. |
|
|
46
|
+
| `isSafeSearchString(value)` | Trimmed string that starts with a letter and then uses letters, numbers, spaces, `_`, `-`, `.`, `,`, `'`, `(`, `)`, or `@`. |
|
|
47
|
+
| `isEmailString(value)` | Email-like string up to 320 chars using the package email regex. |
|
|
48
|
+
| `isJwtString(value)` | Non-blank JWT-like string up to 8192 chars that starts with `eyJ` and contains only letters, numbers, `-`, `_`, and `.`. |
|
|
49
|
+
| `isPasswordString(value)` | 6 to 32 chars from letters, numbers, and `!@#$%^&*_-`, with at least one alphanumeric char and one listed special char. |
|
|
50
|
+
| `isSimplePasswordString(value)` | 6 to 32 chars from letters, numbers, and `!@#$%^&*_-`. |
|
|
51
|
+
| `isPasswordStringFailureMessage(value)` | `null` when `isPasswordString` passes, otherwise a human-readable failure message. |
|
|
52
|
+
| `isSimplePasswordStringFailureMessage(value)` | `null` when `isSimplePasswordString` passes, otherwise a human-readable failure message. |
|
|
53
|
+
| `isUsernameString(value)` | 1 to 200 alphanumeric chars. |
|
|
54
|
+
| `isPhoneNumber(value)` | North American 10-digit phone number with optional parentheses around the area code and optional space, `-`, or `.` separators. |
|
|
55
|
+
| `isUrlSafeString(value)` | Non-blank string up to 2048 chars using letters, numbers, `-`, `_`, and `.`. |
|
|
56
|
+
| `isString6To24CharacterLong(value)` | String with length from 6 to 24. |
|
|
57
|
+
| `isString6To16CharacterLong(value)` | String with length from 6 to 16. |
|
|
58
|
+
| `isProvinceString(value)` | `ON` or `QC`, case-insensitive. |
|
|
59
|
+
| `isBoolValue(value)` | Boolean `true`/`false` or strings `"true"`/`"false"`. |
|
|
60
|
+
| `isPostalCodeString(value)` | Canadian postal code format, case-insensitive, with optional middle space. |
|
|
61
|
+
| `isSafeString(value)` | 1 to 10000 chars using letters, numbers, spaces, `-`, `_`, `.`, `,`, `#`, `*`, `'`, `(`, `)`, `[`, `]`, or `:`. |
|
|
62
|
+
| `isInStringArray(array, value)` | `value`, after lowercase/trim validation as a name string, is included in the supplied array. |
|
|
63
|
+
| `isCountryCodeString(value)` | Country dialing code in `+1` to `+999` format. |
|
|
64
|
+
| `isValidDomainName(value)` | Domain name with at least one dot, valid DNS-like labels, and max total length 253. |
|
|
65
|
+
| `isValidTimestampzString(value)` | ISO 8601 timestamp with `Z` or `+/-HH:MM` timezone offset. |
|
|
66
|
+
| `isValidTimestampString(value)` | ISO 8601 timestamp without timezone offset. |
|
|
67
|
+
| `isValidUrl(value)` | Absolute `http://` or `https://` URL up to 2048 chars. |
|
|
68
|
+
| `isValidArrayOfStrings(value)` | Array where every element passes `isSafeString`. |
|
|
69
|
+
|
|
70
|
+
## `validateProperties(obj)`
|
|
71
|
+
|
|
72
|
+
`validateProperties` accepts an object and returns a new object that contains
|
|
73
|
+
only recognized keys whose values pass the validator assigned to that key.
|
|
74
|
+
Unknown keys and invalid values are silently omitted. `null`, `undefined`, or no
|
|
75
|
+
argument returns `{}`.
|
|
76
|
+
|
|
77
|
+
```js
|
|
78
|
+
const input = {
|
|
79
|
+
first_name: 'Jane',
|
|
80
|
+
email: 'jane@example.com',
|
|
81
|
+
phone_number: '123',
|
|
82
|
+
unknown_key: 'ignored',
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
validateProperties(input);
|
|
86
|
+
// {
|
|
87
|
+
// first_name: 'Jane',
|
|
88
|
+
// email: 'jane@example.com'
|
|
89
|
+
// }
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Supported Property Keys
|
|
93
|
+
|
|
94
|
+
Keys are matched exactly. Both snake_case and camelCase variants are listed
|
|
95
|
+
where the package supports both.
|
|
96
|
+
|
|
97
|
+
| Validator | Keys |
|
|
98
|
+
| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
99
|
+
| `isNameString` | `first_name`, `firstName`, `last_name`, `lastName`, `username`, `new_status`, `newStatus`, `description`, `comment`, `status`, `name`, `title`, `brand`, `short_description`, `shortDescription`, `college_name`, `collegeName`, `campus_name`, `campusName`, `role`, `role_id`, `roleId`, `campus`, `institution_name`, `institutionName`, `program_name`, `programName`, `role_name`, `roleName`, `document_type`, `documentType`, `reason`, `street`, `city`, `state`, `country`, `type` |
|
|
100
|
+
| `isCharactersString` | `postal_code`, `postalCode`, `period` |
|
|
101
|
+
| `isBoolValue` | `is_primary`, `isPrimary`, `active` |
|
|
102
|
+
| `isSafeSearchString` | `search_string`, `searchString` |
|
|
103
|
+
| `isString6To16CharacterLong` and `isSimplePasswordString` | `password`, `new_password`, `newPassword` |
|
|
104
|
+
| `isString6To16CharacterLong` and `isPasswordString` | `strong_password`, `strongPassword` |
|
|
105
|
+
| `isEmailString` | `email` |
|
|
106
|
+
| `isPhoneNumber` | `phone_number`, `phoneNumber` |
|
|
107
|
+
| `isUrlSafeString` | `token`, `email_confirm_token`, `emailConfirmToken`, `verification_token`, `verificationToken` |
|
|
108
|
+
| `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` |
|
|
109
|
+
| `isValidIntegerString` | `offset_number`, `offsetNumber`, `number_of_orders`, `numberOfOrders`, `price`, `from`, `number` |
|
|
110
|
+
| `isValidJsonString` on the raw value | `about` |
|
|
111
|
+
| `isValidJsonString(JSON.stringify(value))` | `weight`, `dimensions`, `permission`, `scope_data`, `scopeData`, `meta_data`, `metaData` |
|
|
112
|
+
| `isValidArrayOfStrings` | `aliases` |
|
|
113
|
+
| `isImageUrl` or `isValidUrl` | `image_url`, `imageUrl`, `website`, `file_url`, `fileUrl` |
|
|
114
|
+
| `isValidDomainName` | `domain_name`, `domainName`, `domain`, `email_domain`, `emailDomain`, `email_domain_name`, `emailDomainName` |
|
|
115
|
+
| `isValidTimestampzString` or `isValidTimestampString` | `expires_at`, `expiresAt` |
|
|
116
|
+
|
|
117
|
+
## `validateWhitelistProperties(inputObject, requiredProperties, options)`
|
|
118
|
+
|
|
119
|
+
`validateWhitelistProperties` extracts only the required and optional property
|
|
120
|
+
paths you provide, validates each leaf through `validateProperties`, and returns
|
|
121
|
+
a `Promise<Record<string, any>>` with the sanitized output.
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
const body = {
|
|
125
|
+
first_name: 'Jane',
|
|
126
|
+
email: 'jane@example.com',
|
|
127
|
+
role: 'Admin',
|
|
128
|
+
extra: '<script>',
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const out = await validateWhitelistProperties(body, ['first_name', 'email'], {
|
|
132
|
+
optionalProperties: ['role'],
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// {
|
|
136
|
+
// first_name: 'Jane',
|
|
137
|
+
// email: 'jane@example.com',
|
|
138
|
+
// role: 'Admin'
|
|
139
|
+
// }
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Options
|
|
143
|
+
|
|
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. |
|
|
149
|
+
|
|
150
|
+
### Required And Optional Values
|
|
151
|
+
|
|
152
|
+
Required paths must exist and pass validation. Missing or invalid required paths
|
|
153
|
+
throw a CareCard bad input error.
|
|
154
|
+
|
|
155
|
+
```js
|
|
156
|
+
await validateWhitelistProperties({ email: 'bad' }, ['email']);
|
|
157
|
+
// throws/rejects with:
|
|
158
|
+
// {
|
|
159
|
+
// code: 'BAD_INPUT',
|
|
160
|
+
// message: 'Bad_Input',
|
|
161
|
+
// userMessage: 'Missing or invalid property: email'
|
|
162
|
+
// }
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Optional paths are ignored when absent, but invalid when present.
|
|
166
|
+
|
|
167
|
+
```js
|
|
168
|
+
await validateWhitelistProperties({ first_name: 'Jane', email: 'bad' }, ['first_name'], {
|
|
169
|
+
optionalProperties: ['email'],
|
|
170
|
+
});
|
|
171
|
+
// userMessage: 'Invalid property value: email'
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Nested Paths
|
|
175
|
+
|
|
176
|
+
Use dot notation for nested objects. The leaf key decides which validator is
|
|
177
|
+
used.
|
|
178
|
+
|
|
179
|
+
```js
|
|
180
|
+
const out = await validateWhitelistProperties(
|
|
181
|
+
{
|
|
182
|
+
user: {
|
|
183
|
+
first_name: 'Jane',
|
|
184
|
+
contact: { email: 'jane@example.com', ignored: 'x' },
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
['user.first_name', 'user.contact.email'],
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
// {
|
|
191
|
+
// user: {
|
|
192
|
+
// first_name: 'Jane',
|
|
193
|
+
// contact: { email: 'jane@example.com' }
|
|
194
|
+
// }
|
|
195
|
+
// }
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Nested paths support up to 5 segments. The combined count of required and
|
|
199
|
+
optional paths must be 5000 or fewer.
|
|
200
|
+
|
|
201
|
+
### Arrays
|
|
202
|
+
|
|
203
|
+
If a whitelisted leaf value is an array, each element is validated as if it were
|
|
204
|
+
the scalar value for that same leaf key. The array is accepted only when every
|
|
205
|
+
element passes. Empty arrays are accepted.
|
|
206
|
+
|
|
207
|
+
```js
|
|
208
|
+
await validateWhitelistProperties({ email: ['a@example.com', 'b@example.com'] }, ['email']);
|
|
209
|
+
// { email: ['a@example.com', 'b@example.com'] }
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
This array behavior is intended for repeated scalar fields such as `email` or
|
|
213
|
+
`name`.
|
|
214
|
+
|
|
215
|
+
### Case Conversion And Flattening
|
|
8
216
|
|
|
9
217
|
```js
|
|
10
|
-
|
|
218
|
+
const out = await validateWhitelistProperties({ userInfo: { firstName: 'Jane', phoneNumber: '4165551234' } }, ['userInfo.firstName'], {
|
|
219
|
+
optionalProperties: ['userInfo.phoneNumber'],
|
|
220
|
+
convertToSnakeCase: true,
|
|
221
|
+
flattenOutput: true,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// {
|
|
225
|
+
// 'user_info.first_name': 'Jane',
|
|
226
|
+
// 'user_info.phone_number': '4165551234'
|
|
227
|
+
// }
|
|
11
228
|
```
|
|
12
229
|
|
|
13
|
-
|
|
230
|
+
With `flattenOutput: false` or no `flattenOutput` option, nested paths keep the
|
|
231
|
+
nested output shape:
|
|
14
232
|
|
|
15
233
|
```js
|
|
16
|
-
|
|
234
|
+
const input = { a: { b: { c: { d: { email: 'jane@example.com' } } } } };
|
|
235
|
+
await validateWhitelistProperties(input, ['a.b.c.d.email']);
|
|
236
|
+
// { a: { b: { c: { d: { email: 'jane@example.com' } } } } }
|
|
17
237
|
```
|
|
18
238
|
|
|
19
|
-
|
|
239
|
+
With `flattenOutput: true`, sibling leaves from the same nested parent are
|
|
240
|
+
returned as top-level leaf keys:
|
|
20
241
|
|
|
21
242
|
```js
|
|
22
|
-
|
|
243
|
+
const input = { a: { b: { c: { d: { email: 'jane@example.com', name: 'Jane' } } } } };
|
|
244
|
+
await validateWhitelistProperties(input, ['a.b.c.d.email', 'a.b.c.d.name'], {
|
|
245
|
+
flattenOutput: true,
|
|
246
|
+
});
|
|
247
|
+
// { email: 'jane@example.com', name: 'Jane' }
|
|
23
248
|
```
|
|
24
249
|
|
|
25
|
-
-
|
|
250
|
+
If direct leaf-key flattening produces duplicate keys at different nesting
|
|
251
|
+
levels, the higher-level value is kept and the lower-level duplicate is
|
|
252
|
+
discarded:
|
|
26
253
|
|
|
27
254
|
```js
|
|
28
|
-
|
|
255
|
+
const input = {
|
|
256
|
+
name: 'Top Level Name',
|
|
257
|
+
user: { name: 'Nested Name', email: 'jane@example.com' },
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
await validateWhitelistProperties(input, ['name', 'user.name', 'user.email'], {
|
|
261
|
+
flattenOutput: true,
|
|
262
|
+
});
|
|
263
|
+
// { name: 'Top Level Name', email: 'jane@example.com' }
|
|
29
264
|
```
|
|
265
|
+
|
|
266
|
+
## TypeScript
|
|
267
|
+
|
|
268
|
+
The package ships `index.d.ts` and declares types for the CommonJS exports.
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
import { validateWhitelistProperties, isEmailString } from '@carecard/validate';
|
|
272
|
+
|
|
273
|
+
const valid: boolean = isEmailString('jane@example.com');
|
|
274
|
+
const output: Record<string, any> = await validateWhitelistProperties({ first_name: 'Jane' }, ['first_name']);
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Project Layout
|
|
278
|
+
|
|
279
|
+
```text
|
|
280
|
+
index.js CommonJS public entry point
|
|
281
|
+
index.d.ts TypeScript declarations
|
|
282
|
+
lib/validate.js Direct value validators
|
|
283
|
+
lib/validateProperties.js Key-to-validator sanitizer
|
|
284
|
+
lib/validateWhitelistProperties.js Required/optional whitelist validator
|
|
285
|
+
test/*.test.js Mocha runtime tests
|
|
286
|
+
test/types.test.ts TypeScript declaration tests
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## Development
|
|
290
|
+
|
|
291
|
+
```sh
|
|
292
|
+
npm ci
|
|
293
|
+
npm run test
|
|
294
|
+
npm run test:types
|
|
295
|
+
npm run test:All
|
|
296
|
+
npm run lint
|
|
297
|
+
npm run format:check
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
CI runs on Node.js 25 and executes `npm run test:All`. Publishing to npm happens
|
|
301
|
+
from `main` through the `Publish to npm` GitHub workflow.
|