@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 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, joined by `.` (e.g. `{ 'user.first_name': 'Jane' }`).
17
- * No nested objects remain in the output. Applied after snake_case conversion.
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 list of additional allowed leaf paths and case-conversion flag.
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 so every leaf is a top-level key,
185
- * joined by `.` (`flattenOutput`). Applied after snake_case conversion.
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 (joined by `.`), with no nested objects in the output.
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: produce a flat object with dot-joined keys.
354
+ // 6. Optional flattening.
275
355
  if (flattenOutput) {
276
- validatedObject = flattenObject(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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carecard/validate",
3
- "version": "3.1.19",
3
+ "version": "3.1.20",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/CareCard-ca/pkg-validate.git"
package/readme.md CHANGED
@@ -1,29 +1,301 @@
1
- #Validate
1
+ # @carecard/validate
2
2
 
3
- ### Functions
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
- All functions return boolean value, false on failure
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
- - Validates that a string is string of characters
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
- isStringOfCharacters(str);
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
- - Validates that a string is fit for username
230
+ With `flattenOutput: false` or no `flattenOutput` option, nested paths keep the
231
+ nested output shape:
14
232
 
15
233
  ```js
16
- isStringOfUsername(str);
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
- - Validates that string is an email
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
- isEmail(email);
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
- - Validates that string is fit for password
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
- isStringOfPassword(password);
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.