@carecard/validate 3.1.19 → 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 +12 -3
- package/lib/validateWhitelistProperties.js +49 -6
- package/package.json +1 -1
- package/readme.md +345 -11
package/index.d.ts
CHANGED
|
@@ -13,10 +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
|
|
17
|
-
*
|
|
16
|
+
* becomes a top-level key. No nested objects remain in the output. Applied
|
|
17
|
+
* after snake_case conversion.
|
|
18
18
|
*/
|
|
19
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';
|
|
20
28
|
}
|
|
21
29
|
|
|
22
30
|
/**
|
|
@@ -38,10 +46,11 @@ export interface ValidateWhitelistPropertiesOptions {
|
|
|
38
46
|
* validated elements (e.g. `{ name: ["First", "Other"] }` is validated like
|
|
39
47
|
* `{ name: "First" }` and `{ name: "Other" }` individually).
|
|
40
48
|
* - Optionally converts the resulting keys (including nested keys) to snake_case.
|
|
49
|
+
* - Optionally flattens the result after snake_case conversion.
|
|
41
50
|
*
|
|
42
51
|
* @param inputObject The input object (e.g. `req.body` or `req.params`).
|
|
43
52
|
* @param requiredProperties Leaf paths that must be present and valid. Dot-notation supported.
|
|
44
|
-
* @param options Optional
|
|
53
|
+
* @param options Optional additional leaf paths plus output transformation flags.
|
|
45
54
|
*/
|
|
46
55
|
export function validateWhitelistProperties(
|
|
47
56
|
inputObject: Record<string, any>,
|
|
@@ -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
|
|
185
|
-
*
|
|
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
|
|
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
|
|
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
package/readme.md
CHANGED
|
@@ -1,29 +1,363 @@
|
|
|
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
|
+
### 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
|
+
|
|
175
|
+
### Options
|
|
176
|
+
|
|
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
|
+
```
|
|
199
|
+
|
|
200
|
+
### Required And Optional Values
|
|
201
|
+
|
|
202
|
+
Required paths must exist and pass validation. Missing or invalid required paths
|
|
203
|
+
throw a CareCard bad input error.
|
|
8
204
|
|
|
9
205
|
```js
|
|
10
|
-
|
|
206
|
+
await validateWhitelistProperties({ email: 'bad' }, ['email']);
|
|
207
|
+
// throws/rejects with:
|
|
208
|
+
// {
|
|
209
|
+
// code: 'BAD_INPUT',
|
|
210
|
+
// message: 'Bad_Input',
|
|
211
|
+
// userMessage: 'Missing or invalid property: email'
|
|
212
|
+
// }
|
|
11
213
|
```
|
|
12
214
|
|
|
13
|
-
|
|
215
|
+
Optional paths are ignored when absent, but invalid when present.
|
|
14
216
|
|
|
15
217
|
```js
|
|
16
|
-
|
|
218
|
+
await validateWhitelistProperties({ first_name: 'Jane', email: 'bad' }, ['first_name'], {
|
|
219
|
+
optionalProperties: ['email'],
|
|
220
|
+
});
|
|
221
|
+
// userMessage: 'Invalid property value: email'
|
|
17
222
|
```
|
|
18
223
|
|
|
19
|
-
|
|
224
|
+
### Nested Paths
|
|
225
|
+
|
|
226
|
+
Use dot notation for nested objects. The leaf key decides which validator is
|
|
227
|
+
used.
|
|
20
228
|
|
|
21
229
|
```js
|
|
22
|
-
|
|
230
|
+
const out = await validateWhitelistProperties(
|
|
231
|
+
{
|
|
232
|
+
user: {
|
|
233
|
+
first_name: 'Jane',
|
|
234
|
+
contact: { email: 'jane@example.com', ignored: 'x' },
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
['user.first_name', 'user.contact.email'],
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
// {
|
|
241
|
+
// user: {
|
|
242
|
+
// first_name: 'Jane',
|
|
243
|
+
// contact: { email: 'jane@example.com' }
|
|
244
|
+
// }
|
|
245
|
+
// }
|
|
23
246
|
```
|
|
24
247
|
|
|
25
|
-
|
|
248
|
+
Nested paths support up to 5 segments. The combined count of required and
|
|
249
|
+
optional paths must be 5000 or fewer.
|
|
250
|
+
|
|
251
|
+
### Arrays
|
|
252
|
+
|
|
253
|
+
If a whitelisted leaf value is an array, each element is validated as if it were
|
|
254
|
+
the scalar value for that same leaf key. The array is accepted only when every
|
|
255
|
+
element passes. Empty arrays are accepted.
|
|
26
256
|
|
|
27
257
|
```js
|
|
28
|
-
|
|
258
|
+
await validateWhitelistProperties({ email: ['a@example.com', 'b@example.com'] }, ['email']);
|
|
259
|
+
// { email: ['a@example.com', 'b@example.com'] }
|
|
29
260
|
```
|
|
261
|
+
|
|
262
|
+
This array behavior is intended for repeated scalar fields such as `email` or
|
|
263
|
+
`name`.
|
|
264
|
+
|
|
265
|
+
### Case Conversion And Flattening
|
|
266
|
+
|
|
267
|
+
```js
|
|
268
|
+
const out = await validateWhitelistProperties({ userInfo: { firstName: 'Jane', phoneNumber: '4165551234' } }, ['userInfo.firstName'], {
|
|
269
|
+
optionalProperties: ['userInfo.phoneNumber'],
|
|
270
|
+
convertToSnakeCase: true,
|
|
271
|
+
flattenOutput: true,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// {
|
|
275
|
+
// 'user_info.first_name': 'Jane',
|
|
276
|
+
// 'user_info.phone_number': '4165551234'
|
|
277
|
+
// }
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
With `flattenOutput: false` or no `flattenOutput` option, nested paths keep the
|
|
281
|
+
nested output shape:
|
|
282
|
+
|
|
283
|
+
```js
|
|
284
|
+
const input = { a: { b: { c: { d: { email: 'jane@example.com' } } } } };
|
|
285
|
+
await validateWhitelistProperties(input, ['a.b.c.d.email']);
|
|
286
|
+
// { a: { b: { c: { d: { email: 'jane@example.com' } } } } }
|
|
287
|
+
```
|
|
288
|
+
|
|
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:
|
|
300
|
+
|
|
301
|
+
```js
|
|
302
|
+
const input = { a: { b: { c: { d: { email: 'jane@example.com', name: 'Jane' } } } } };
|
|
303
|
+
await validateWhitelistProperties(input, ['a.b.c.d.email', 'a.b.c.d.name'], {
|
|
304
|
+
flattenOutput: true,
|
|
305
|
+
flattenKeyStyle: 'leaf',
|
|
306
|
+
});
|
|
307
|
+
// { email: 'jane@example.com', name: 'Jane' }
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
When `flattenKeyStyle: 'leaf'` produces duplicate keys at different nesting
|
|
311
|
+
levels, the higher-level value is kept and the lower-level duplicate is
|
|
312
|
+
discarded. Duplicate leaf keys at the same nesting depth keep the first value
|
|
313
|
+
encountered:
|
|
314
|
+
|
|
315
|
+
```js
|
|
316
|
+
const input = {
|
|
317
|
+
name: 'Top Level Name',
|
|
318
|
+
user: { name: 'Nested Name', email: 'jane@example.com' },
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
await validateWhitelistProperties(input, ['name', 'user.name', 'user.email'], {
|
|
322
|
+
flattenOutput: true,
|
|
323
|
+
flattenKeyStyle: 'leaf',
|
|
324
|
+
});
|
|
325
|
+
// { name: 'Top Level Name', email: 'jane@example.com' }
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
## TypeScript
|
|
329
|
+
|
|
330
|
+
The package ships `index.d.ts` and declares types for the CommonJS exports.
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
import { validateWhitelistProperties, isEmailString } from '@carecard/validate';
|
|
334
|
+
|
|
335
|
+
const valid: boolean = isEmailString('jane@example.com');
|
|
336
|
+
const output: Record<string, any> = await validateWhitelistProperties({ first_name: 'Jane' }, ['first_name']);
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
## Project Layout
|
|
340
|
+
|
|
341
|
+
```text
|
|
342
|
+
index.js CommonJS public entry point
|
|
343
|
+
index.d.ts TypeScript declarations
|
|
344
|
+
lib/validate.js Direct value validators
|
|
345
|
+
lib/validateProperties.js Key-to-validator sanitizer
|
|
346
|
+
lib/validateWhitelistProperties.js Required/optional whitelist validator
|
|
347
|
+
test/*.test.js Mocha runtime tests
|
|
348
|
+
test/types.test.ts TypeScript declaration tests
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
## Development
|
|
352
|
+
|
|
353
|
+
```sh
|
|
354
|
+
npm ci
|
|
355
|
+
npm run test
|
|
356
|
+
npm run test:types
|
|
357
|
+
npm run test:All
|
|
358
|
+
npm run lint
|
|
359
|
+
npm run format:check
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
CI runs on Node.js 25 and executes `npm run test:All`. Publishing to npm happens
|
|
363
|
+
from `main` through the `Publish to npm` GitHub workflow.
|