@carecard/validate 3.1.24 → 3.1.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/config.toml +2 -0
- package/.agents/skills/carecard-workspace-standards/SKILL.md +401 -0
- package/.agents/skills/carecard-workspace-standards/agents/openai.yaml +4 -0
- package/.agents/skills/github-pr-create-update/SKILL.md +188 -0
- package/.agents/skills/github-pr-create-update/agents/openai.yaml +5 -0
- package/.agents/skills/github-pr-merge-cleanup/SKILL.md +175 -0
- package/.agents/skills/github-pr-merge-cleanup/agents/openai.yaml +5 -0
- package/.agents/skills/logged-in-user-profile-page/SKILL.md +33 -0
- package/.agents/skills/logged-in-user-profile-page/agents/openai.yaml +4 -0
- package/.agents/skills/pkg-validate-validation-library/SKILL.md +200 -0
- package/.agents/skills/pkg-validate-validation-library/agents/openai.yaml +4 -0
- package/.agents/skills/software-design-patterns-and-clean-code/SKILL.md +73 -0
- package/.codex/config.toml +2 -0
- package/.github/workflows/ci.yml +2 -2
- package/.husky/pre-commit +4 -2
- package/index.d.ts +12 -3
- package/lib/validateProperties.js +7 -0
- package/lib/validateWhitelistProperties.js +49 -6
- package/package.json +6 -6
- package/readme.md +14 -0
|
@@ -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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carecard/validate",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.25",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/CareCard-ca/pkg-validate.git"
|
|
@@ -27,18 +27,18 @@
|
|
|
27
27
|
"license": "ISC",
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/mocha": "10.0.10",
|
|
30
|
-
"@types/node": "25.
|
|
30
|
+
"@types/node": "25.9.1",
|
|
31
31
|
"eslint": "9.39.4",
|
|
32
32
|
"husky": "9.1.7",
|
|
33
|
-
"lint-staged": "17.0.
|
|
34
|
-
"mocha": "11.7.
|
|
33
|
+
"lint-staged": "17.0.5",
|
|
34
|
+
"mocha": "11.7.6",
|
|
35
35
|
"nyc": "18.0.0",
|
|
36
36
|
"prettier": "3.8.3",
|
|
37
37
|
"ts-node": "10.9.2",
|
|
38
|
-
"typescript": "
|
|
38
|
+
"typescript": "6.0.3"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@carecard/common-util": "
|
|
41
|
+
"@carecard/common-util": "3.1.15"
|
|
42
42
|
},
|
|
43
43
|
"nyc": {
|
|
44
44
|
"all": true,
|
package/readme.md
CHANGED
|
@@ -106,6 +106,7 @@ where the package supports both.
|
|
|
106
106
|
| `isString6To16CharacterLong` and `isPasswordString` | `strong_password`, `strongPassword` |
|
|
107
107
|
| `isEmailString` | `email` |
|
|
108
108
|
| `isPhoneNumber` | `phone_number`, `phoneNumber` |
|
|
109
|
+
| `isCountryCodeString` | `country_code`, `countryCode` |
|
|
109
110
|
| `isUrlSafeString` | `token`, `email_confirm_token`, `emailConfirmToken`, `verification_token`, `verificationToken` |
|
|
110
111
|
| `isValidUuidString` | `uuid`, `item_id`, `itemId`, `user_id`, `userId`, `address_id`, `addressId`, `image_id`, `imageId`, `order_id`, `orderId`, `category_id`, `categoryId`, `parent_id`, `parentId`, `college_id`, `collegeId`, `campus_id`, `campusId`, `program_id`, `programId`, `id`, `institution_id`, `institutionId`, `role_assignment_id`, `roleAssignmentId`, `user_role_id`, `userRoleId`, `phone_number_id`, `phoneNumberId`, `entity_id`, `entityId`, `changed_by`, `changedBy`, `request_id`, `requestId` |
|
|
111
112
|
| `isValidIntegerString` | `offset_number`, `offsetNumber`, `number_of_orders`, `numberOfOrders`, `price`, `from`, `number`, `limit`, `offset` |
|
|
@@ -363,3 +364,16 @@ npm run format:check
|
|
|
363
364
|
|
|
364
365
|
CI runs on Node.js 25 and executes `npm run test:All`. Publishing to npm happens
|
|
365
366
|
from `main` through the `Publish to npm` GitHub workflow.
|
|
367
|
+
|
|
368
|
+
## Auth Boundary
|
|
369
|
+
|
|
370
|
+
Validation protects request shape, not authorization. `ms-auth` owns its
|
|
371
|
+
auth-table RLS contract: normal users are self-row only, JWT `roles: ["ad"]`
|
|
372
|
+
is the auth super-admin signal, and public auth flows use narrow system
|
|
373
|
+
contexts. Do not use validators as a replacement for service RLS or database
|
|
374
|
+
context checks.
|
|
375
|
+
|
|
376
|
+
Docs that mention `ms-auth` controller internals should use concise action
|
|
377
|
+
names such as `loginUser`, `registerUser`, `getUserDetail`, and `renewJwt`.
|
|
378
|
+
Access level is conveyed by route middleware and endpoint placement, not by
|
|
379
|
+
`public`/`protected`/`admin`/`Handler` suffixes.
|