@carecard/validate 3.1.20 → 3.1.22
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/.github/workflows/auto-draft-pr.yml +69 -10
- package/index.d.ts +13 -5
- package/lib/validate.js +10 -0
- package/lib/validateProperties.js +6 -1
- package/lib/validateWhitelistProperties.js +18 -57
- package/package.json +1 -1
- package/readme.md +92 -28
|
@@ -36,6 +36,7 @@ on:
|
|
|
36
36
|
|
|
37
37
|
permissions:
|
|
38
38
|
contents: read
|
|
39
|
+
issues: write
|
|
39
40
|
pull-requests: write
|
|
40
41
|
|
|
41
42
|
jobs:
|
|
@@ -84,6 +85,8 @@ jobs:
|
|
|
84
85
|
HEAD: ${{ steps.target.outputs.head }}
|
|
85
86
|
BASE: ${{ steps.target.outputs.base }}
|
|
86
87
|
REPO: ${{ github.repository }}
|
|
88
|
+
PR_ASSIGNEE: ${{ github.actor }}
|
|
89
|
+
PR_REVIEWER: singh-pankaj-k
|
|
87
90
|
run: |
|
|
88
91
|
set -euo pipefail
|
|
89
92
|
|
|
@@ -98,17 +101,73 @@ jobs:
|
|
|
98
101
|
--jq '.[0].number' || true)
|
|
99
102
|
|
|
100
103
|
if [[ -n "${EXISTING:-}" ]]; then
|
|
101
|
-
echo "An open PR already exists for $HEAD -> $BASE (#$EXISTING).
|
|
102
|
-
|
|
104
|
+
echo "An open PR already exists for $HEAD -> $BASE (#$EXISTING). Updating PR metadata."
|
|
105
|
+
PR_NUMBER="$EXISTING"
|
|
106
|
+
else
|
|
107
|
+
TITLE="Draft: merge \`$HEAD\` into \`$BASE\`"
|
|
108
|
+
BODY=$'Auto-created draft PR for branch `'"$HEAD"$'`.\n\nTarget base: `'"$BASE"$'`\n\nMark this PR as ready for review when you want it to be reviewed/merged.'
|
|
109
|
+
|
|
110
|
+
gh pr create \
|
|
111
|
+
--repo "$REPO" \
|
|
112
|
+
--draft \
|
|
113
|
+
--base "$BASE" \
|
|
114
|
+
--head "$HEAD" \
|
|
115
|
+
--title "$TITLE" \
|
|
116
|
+
--body "$BODY"
|
|
117
|
+
|
|
118
|
+
PR_NUMBER=$(gh pr list \
|
|
119
|
+
--repo "$REPO" \
|
|
120
|
+
--state open \
|
|
121
|
+
--head "$HEAD" \
|
|
122
|
+
--base "$BASE" \
|
|
123
|
+
--json number \
|
|
124
|
+
--jq '.[0].number')
|
|
103
125
|
fi
|
|
104
126
|
|
|
105
|
-
|
|
106
|
-
|
|
127
|
+
gh pr edit "$PR_NUMBER" \
|
|
128
|
+
--repo "$REPO" \
|
|
129
|
+
--add-assignee "$PR_ASSIGNEE"
|
|
107
130
|
|
|
108
|
-
gh pr
|
|
131
|
+
PR_AUTHOR=$(gh pr view "$PR_NUMBER" \
|
|
109
132
|
--repo "$REPO" \
|
|
110
|
-
--
|
|
111
|
-
--
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
133
|
+
--json author \
|
|
134
|
+
--jq '.author.login')
|
|
135
|
+
|
|
136
|
+
if [[ "$PR_AUTHOR" == "$PR_REVIEWER" ]]; then
|
|
137
|
+
echo "Skipping reviewer request because $PR_REVIEWER is the PR author."
|
|
138
|
+
else
|
|
139
|
+
gh pr edit "$PR_NUMBER" \
|
|
140
|
+
--repo "$REPO" \
|
|
141
|
+
--add-reviewer "$PR_REVIEWER"
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
- name: Assign matching issue ticket (best effort)
|
|
145
|
+
if: steps.target.outputs.skip == 'false'
|
|
146
|
+
env:
|
|
147
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
148
|
+
HEAD: ${{ steps.target.outputs.head }}
|
|
149
|
+
REPO: ${{ github.repository }}
|
|
150
|
+
PR_ASSIGNEE: ${{ github.actor }}
|
|
151
|
+
run: |
|
|
152
|
+
set -euo pipefail
|
|
153
|
+
|
|
154
|
+
TICKET_NUMBER=""
|
|
155
|
+
if [[ "$HEAD" =~ (^|/)(iss|issue|issues)[/-]?#?([0-9]+)([^0-9]|$) ]]; then
|
|
156
|
+
TICKET_NUMBER="${BASH_REMATCH[3]}"
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
if [[ -z "$TICKET_NUMBER" ]]; then
|
|
160
|
+
echo "No GitHub issue ticket number found in branch '$HEAD'. Skipping ticket assignment."
|
|
161
|
+
exit 0
|
|
162
|
+
fi
|
|
163
|
+
|
|
164
|
+
if ! gh issue view "$TICKET_NUMBER" --repo "$REPO" --json number >/dev/null 2>&1; then
|
|
165
|
+
echo "GitHub issue #$TICKET_NUMBER was not found. Skipping ticket assignment."
|
|
166
|
+
exit 0
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
if gh issue edit "$TICKET_NUMBER" --repo "$REPO" --add-assignee "$PR_ASSIGNEE"; then
|
|
170
|
+
echo "Assigned issue ticket #$TICKET_NUMBER to $PR_ASSIGNEE."
|
|
171
|
+
else
|
|
172
|
+
echo "::warning::Could not assign issue ticket #$TICKET_NUMBER to $PR_ASSIGNEE."
|
|
173
|
+
fi
|
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.
|
|
17
|
-
*
|
|
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
|
/**
|
|
@@ -65,6 +70,8 @@ export function isValidIntegerString(str: any): boolean;
|
|
|
65
70
|
export function isValidUuidString(str: any): boolean;
|
|
66
71
|
/** Checks if the string contains only alphanumeric characters, spaces, underscores, or hyphens. */
|
|
67
72
|
export function isCharactersString(str: any): boolean;
|
|
73
|
+
/** Checks if the string is a valid street address format. */
|
|
74
|
+
export function isStreetString(str: any): boolean;
|
|
68
75
|
/** Checks if the string is a valid name format. */
|
|
69
76
|
export function isNameString(str: any): boolean;
|
|
70
77
|
/** Checks if the string is safe for search queries. */
|
|
@@ -126,6 +133,7 @@ export const validate: {
|
|
|
126
133
|
isValidIntegerString: typeof isValidIntegerString;
|
|
127
134
|
isValidUuidString: typeof isValidUuidString;
|
|
128
135
|
isCharactersString: typeof isCharactersString;
|
|
136
|
+
isStreetString: typeof isStreetString;
|
|
129
137
|
isNameString: typeof isNameString;
|
|
130
138
|
isSafeSearchString: typeof isSafeSearchString;
|
|
131
139
|
isEmailString: typeof isEmailString;
|
package/lib/validate.js
CHANGED
|
@@ -36,6 +36,15 @@ const isCharactersString = str => {
|
|
|
36
36
|
return /^[\da-zA-Z _-]+$/.test(str);
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
+
const isStreetString = str => {
|
|
40
|
+
if (typeof str !== 'string' || str.trim().length === 0 || str.length > 1000) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const value = str.trim();
|
|
44
|
+
const streetRegex = /^(?![,_-])[0-9a-zA-Z\s,./#-]+$/;
|
|
45
|
+
return streetRegex.test(value);
|
|
46
|
+
};
|
|
47
|
+
|
|
39
48
|
const isNameString = str => {
|
|
40
49
|
if (typeof str !== 'string' || str.length === 0 || str.length > 1000) {
|
|
41
50
|
return false;
|
|
@@ -204,6 +213,7 @@ module.exports = {
|
|
|
204
213
|
isValidIntegerString,
|
|
205
214
|
isValidUuidString,
|
|
206
215
|
isCharactersString,
|
|
216
|
+
isStreetString,
|
|
207
217
|
isNameString,
|
|
208
218
|
isSafeSearchString,
|
|
209
219
|
isEmailString,
|
|
@@ -19,6 +19,7 @@ const {
|
|
|
19
19
|
isBoolValue,
|
|
20
20
|
isValidUrl,
|
|
21
21
|
isValidArrayOfStrings,
|
|
22
|
+
isStreetString,
|
|
22
23
|
} = require('./validate');
|
|
23
24
|
|
|
24
25
|
function validateProperties(obj = {}) {
|
|
@@ -62,7 +63,6 @@ function validateProperties(obj = {}) {
|
|
|
62
63
|
case 'entityType':
|
|
63
64
|
case 'action_type':
|
|
64
65
|
case 'actionType':
|
|
65
|
-
case 'street':
|
|
66
66
|
case 'city':
|
|
67
67
|
case 'state':
|
|
68
68
|
case 'country':
|
|
@@ -71,6 +71,11 @@ function validateProperties(obj = {}) {
|
|
|
71
71
|
returnObj[key] = value;
|
|
72
72
|
}
|
|
73
73
|
break;
|
|
74
|
+
case 'street':
|
|
75
|
+
if (isStreetString(value)) {
|
|
76
|
+
returnObj[key] = value;
|
|
77
|
+
}
|
|
78
|
+
break;
|
|
74
79
|
case 'postal_code':
|
|
75
80
|
case 'postalCode':
|
|
76
81
|
if (isCharactersString(value)) {
|
|
@@ -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`).
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
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 =
|
|
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
package/readme.md
CHANGED
|
@@ -42,6 +42,7 @@ a string on failure and `null` on success.
|
|
|
42
42
|
| `isValidIntegerString(value)` | Digit-only string, 1 to 20 chars. No signs or decimals. |
|
|
43
43
|
| `isValidUuidString(value)` | Canonical UUID string in `8-4-4-4-12` format, case-insensitive. |
|
|
44
44
|
| `isCharactersString(value)` | 1 to 1000 chars containing letters, numbers, spaces, `_`, or `-`. |
|
|
45
|
+
| `isStreetString(value)` | Non-empty street-like string up to 1000 chars using letters, numbers, spaces, `,`, `.`, `/`, `#`, or `-`, and not starting with `,`, `_`, or `-`. |
|
|
45
46
|
| `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
47
|
| `isSafeSearchString(value)` | Trimmed string that starts with a letter and then uses letters, numbers, spaces, `_`, `-`, `.`, `,`, `'`, `(`, `)`, or `@`. |
|
|
47
48
|
| `isEmailString(value)` | Email-like string up to 320 chars using the package email regex. |
|
|
@@ -94,25 +95,26 @@ validateProperties(input);
|
|
|
94
95
|
Keys are matched exactly. Both snake_case and camelCase variants are listed
|
|
95
96
|
where the package supports both.
|
|
96
97
|
|
|
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`, `
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
104
|
-
| `isString6To16CharacterLong` and `
|
|
105
|
-
| `
|
|
106
|
-
| `
|
|
107
|
-
| `
|
|
108
|
-
| `
|
|
109
|
-
| `
|
|
110
|
-
| `
|
|
111
|
-
| `isValidJsonString
|
|
112
|
-
| `
|
|
113
|
-
| `
|
|
114
|
-
| `
|
|
115
|
-
| `
|
|
98
|
+
| Validator | Keys |
|
|
99
|
+
| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
100
|
+
| `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`, `entity_type`, `entityType`, `action_type`, `actionType`, `city`, `state`, `country`, `type` |
|
|
101
|
+
| `isStreetString` | `street` |
|
|
102
|
+
| `isCharactersString` | `postal_code`, `postalCode`, `period` |
|
|
103
|
+
| `isBoolValue` | `is_primary`, `isPrimary`, `active` |
|
|
104
|
+
| `isSafeSearchString` | `search_string`, `searchString` |
|
|
105
|
+
| `isString6To16CharacterLong` and `isSimplePasswordString` | `password`, `new_password`, `newPassword` |
|
|
106
|
+
| `isString6To16CharacterLong` and `isPasswordString` | `strong_password`, `strongPassword` |
|
|
107
|
+
| `isEmailString` | `email` |
|
|
108
|
+
| `isPhoneNumber` | `phone_number`, `phoneNumber` |
|
|
109
|
+
| `isUrlSafeString` | `token`, `email_confirm_token`, `emailConfirmToken`, `verification_token`, `verificationToken` |
|
|
110
|
+
| `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
|
+
| `isValidIntegerString` | `offset_number`, `offsetNumber`, `number_of_orders`, `numberOfOrders`, `price`, `from`, `number`, `limit`, `offset` |
|
|
112
|
+
| `isValidJsonString` on the raw value | `about` |
|
|
113
|
+
| `isValidJsonString(JSON.stringify(value))` | `weight`, `dimensions`, `permission`, `scope_data`, `scopeData`, `meta_data`, `metaData` |
|
|
114
|
+
| `isValidArrayOfStrings` | `aliases` |
|
|
115
|
+
| `isImageUrl` or `isValidUrl` | `image_url`, `imageUrl`, `website`, `file_url`, `fileUrl` |
|
|
116
|
+
| `isValidDomainName` | `domain_name`, `domainName`, `domain`, `email_domain`, `emailDomain`, `email_domain_name`, `emailDomainName` |
|
|
117
|
+
| `isValidTimestampzString` or `isValidTimestampString` | `expires_at`, `expiresAt`, `start_time`, `startTime`, `end_time`, `endTime` |
|
|
116
118
|
|
|
117
119
|
## `validateWhitelistProperties(inputObject, requiredProperties, options)`
|
|
118
120
|
|
|
@@ -139,13 +141,63 @@ const out = await validateWhitelistProperties(body, ['first_name', 'email'], {
|
|
|
139
141
|
// }
|
|
140
142
|
```
|
|
141
143
|
|
|
144
|
+
### Defaults
|
|
145
|
+
|
|
146
|
+
When omitted, `requiredProperties` defaults to `[]` and `options` defaults to:
|
|
147
|
+
|
|
148
|
+
```js
|
|
149
|
+
{
|
|
150
|
+
optionalProperties: [],
|
|
151
|
+
convertToSnakeCase: false,
|
|
152
|
+
flattenOutput: false,
|
|
153
|
+
flattenKeyStyle: 'path',
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The default output preserves the nested shape described by whitelisted dot paths.
|
|
158
|
+
`flattenKeyStyle` only changes output when `flattenOutput` is `true`.
|
|
159
|
+
|
|
160
|
+
```js
|
|
161
|
+
const input = {
|
|
162
|
+
user: {
|
|
163
|
+
first_name: 'Jane',
|
|
164
|
+
contact: { email: 'jane@example.com' },
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
await validateWhitelistProperties(input, ['user.first_name', 'user.contact.email']);
|
|
169
|
+
// {
|
|
170
|
+
// user: {
|
|
171
|
+
// first_name: 'Jane',
|
|
172
|
+
// contact: { email: 'jane@example.com' }
|
|
173
|
+
// }
|
|
174
|
+
// }
|
|
175
|
+
```
|
|
176
|
+
|
|
142
177
|
### Options
|
|
143
178
|
|
|
144
|
-
| Option | Default
|
|
145
|
-
| -------------------- |
|
|
146
|
-
| `optionalProperties` | `[]`
|
|
147
|
-
| `convertToSnakeCase` | `false`
|
|
148
|
-
| `flattenOutput` | `false`
|
|
179
|
+
| Option | Default | Behavior |
|
|
180
|
+
| -------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
181
|
+
| `optionalProperties` | `[]` | Additional property paths that may be present. Absent optional paths are ignored. Present optional paths must be valid. |
|
|
182
|
+
| `convertToSnakeCase` | `false` | When `true`, converts returned keys, including nested keys, to snake_case using `@carecard/common-util`. Conversion happens before flattening. |
|
|
183
|
+
| `flattenOutput` | `false` | When `true`, removes nested objects from the returned value so every validated leaf becomes a top-level key. |
|
|
184
|
+
| `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. |
|
|
185
|
+
|
|
186
|
+
Example with only the default options:
|
|
187
|
+
|
|
188
|
+
```js
|
|
189
|
+
await validateWhitelistProperties({ first_name: 'Jane', email: 'jane@example.com', ignored: 'x' }, ['first_name']);
|
|
190
|
+
// { first_name: 'Jane' }
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Example with optional properties:
|
|
194
|
+
|
|
195
|
+
```js
|
|
196
|
+
await validateWhitelistProperties({ first_name: 'Jane', phone_number: '4165551234' }, ['first_name'], {
|
|
197
|
+
optionalProperties: ['phone_number'],
|
|
198
|
+
});
|
|
199
|
+
// { first_name: 'Jane', phone_number: '4165551234' }
|
|
200
|
+
```
|
|
149
201
|
|
|
150
202
|
### Required And Optional Values
|
|
151
203
|
|
|
@@ -236,20 +288,31 @@ await validateWhitelistProperties(input, ['a.b.c.d.email']);
|
|
|
236
288
|
// { a: { b: { c: { d: { email: 'jane@example.com' } } } } }
|
|
237
289
|
```
|
|
238
290
|
|
|
239
|
-
With `flattenOutput: true`,
|
|
240
|
-
|
|
291
|
+
With `flattenOutput: true`, keys are full dot paths by default:
|
|
292
|
+
|
|
293
|
+
```js
|
|
294
|
+
const input = { a: { b: { c: { d: { email: 'jane@example.com', name: 'Jane' } } } } };
|
|
295
|
+
await validateWhitelistProperties(input, ['a.b.c.d.email', 'a.b.c.d.name'], {
|
|
296
|
+
flattenOutput: true,
|
|
297
|
+
});
|
|
298
|
+
// { 'a.b.c.d.email': 'jane@example.com', 'a.b.c.d.name': 'Jane' }
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Use `flattenKeyStyle: 'leaf'` to return top-level leaf keys instead:
|
|
241
302
|
|
|
242
303
|
```js
|
|
243
304
|
const input = { a: { b: { c: { d: { email: 'jane@example.com', name: 'Jane' } } } } };
|
|
244
305
|
await validateWhitelistProperties(input, ['a.b.c.d.email', 'a.b.c.d.name'], {
|
|
245
306
|
flattenOutput: true,
|
|
307
|
+
flattenKeyStyle: 'leaf',
|
|
246
308
|
});
|
|
247
309
|
// { email: 'jane@example.com', name: 'Jane' }
|
|
248
310
|
```
|
|
249
311
|
|
|
250
|
-
|
|
312
|
+
When `flattenKeyStyle: 'leaf'` produces duplicate keys at different nesting
|
|
251
313
|
levels, the higher-level value is kept and the lower-level duplicate is
|
|
252
|
-
discarded
|
|
314
|
+
discarded. Duplicate leaf keys at the same nesting depth keep the first value
|
|
315
|
+
encountered:
|
|
253
316
|
|
|
254
317
|
```js
|
|
255
318
|
const input = {
|
|
@@ -259,6 +322,7 @@ const input = {
|
|
|
259
322
|
|
|
260
323
|
await validateWhitelistProperties(input, ['name', 'user.name', 'user.email'], {
|
|
261
324
|
flattenOutput: true,
|
|
325
|
+
flattenKeyStyle: 'leaf',
|
|
262
326
|
});
|
|
263
327
|
// { name: 'Top Level Name', email: 'jane@example.com' }
|
|
264
328
|
```
|