@carecard/validate 3.1.22 → 3.1.24

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.
@@ -0,0 +1,180 @@
1
+ # Codex Instructions For pkg-validate
2
+
3
+ These instructions apply to the `pkg-validate` repository. This file is self-contained:
4
+ it includes the workspace-level instructions that were previously read from
5
+ `/Users/pankajpriscilla/SO_CareCardCa/.codex/AGENTS.md`, followed by
6
+ repository-specific guidance. Removing the workspace-level `.codex/AGENTS.md`
7
+ must not change the rules for this repository.
8
+
9
+ ## Embedded Workspace Instructions
10
+
11
+ These instructions apply to the whole workspace. The workspace is a collection of independent repositories, not one monorepo. Treat each `api-*`, `pkg-*`, and `app-dashboard` directory as its own project with its own package scripts, Git status, and test commands.
12
+
13
+ ### Non-Negotiable Instructions
14
+
15
+ - **Never use TypeScript type `any`.** Always use specific domain types, generics, `unknown` with proper narrowing, or existing project types.
16
+ - **Always follow the owner's coding style.** Preserve the existing style in the file and repository you are editing.
17
+ - **Always follow the owner's naming conventions.** Use meaningful function, variable, file, test, and type names that match the surrounding code.
18
+ - **Always follow the existing project structure.** Put code, tests, docs, services, validation, transforms, components, and helpers where the current repository already expects them.
19
+ - **Always use Test-Driven Development.** Write or update focused tests first, verify they fail for the missing behavior when practical, then implement the code.
20
+ - **Never suppress errors, TypeScript errors, linter warnings, or failing tests.** Do not add `eslint-disable`, `@ts-ignore`, broad catches, empty catches, or other suppression unless the user explicitly requests it. Handle the issue properly.
21
+ - **Do not add new dependencies unless they are clearly necessary.** If a dependency might be needed, stop and ask for confirmation first, with a clear reason, tradeoff, and why existing code cannot reasonably solve it.
22
+ - **Before finalizing any response for a repository, run every script in that repository's `.husky` directory.** Do not bypass hooks. If a `.husky` script fails, fix the underlying issue and rerun it. If it cannot run because of environment constraints, report the exact script and reason.
23
+
24
+ ### Core Coding Principles
25
+
26
+ - Prefer minimal dependencies. Do not add libraries or frameworks unless the existing stack cannot reasonably solve the problem.
27
+ - Prefer implementing core logic directly with readable code over adding abstractions or packages.
28
+ - Explain architectural tradeoffs before major changes, especially changes that affect shared packages, API contracts, security, persistence, authentication, or frontend/backend boundaries.
29
+ - Favor readable, maintainable code over short clever code.
30
+ - Preserve the existing project style, file structure, naming style, module system, and test framework.
31
+ - Use Test-Driven Development: write or update focused tests first, then implement the code.
32
+ - Use meaningful function and variable names. Names should expose intent and domain behavior.
33
+ - Use specific types everywhere. Do not use `any`.
34
+ - Keep changes scoped and easy to review. Avoid unrelated formatting churn or opportunistic refactors.
35
+ - Use postgres functions and stored procedures instead of raw SQL.
36
+ - Use postgres and database search and other functionalities instead of doing it in controllers.
37
+ - Use different type postgres searches, like fuzzy search, trigram search, full-text search, and vector search.
38
+ - When possible, push the complexity of data saving, edit and access to the database.
39
+
40
+ ### Repo Workflow
41
+
42
+ - Work from the specific project directory you are changing, such as `api-auth`, `api-contact-us`, `api-institutions`, `pkg-common-util`, or `app-dashboard`.
43
+ - Check local status inside the affected project before editing. These directories are independent Git repositories.
44
+ - Do not revert or overwrite changes you did not make.
45
+ - Before finishing a code change, run the relevant tests and lint/format checks for the affected project.
46
+ - Before finalizing any response after code changes, run all validation commands required by the affected repository. This includes relevant package scripts, all commands in `.husky` hooks, and all scripts or documented validation commands in `.junie`.
47
+ - When a project has a `.junie` directory, read the applicable `.junie` guidance for that repository before editing. Before the final response, run every executable script in `.junie` and every validation/test command explicitly documented there. Fix any issues those commands report before finalizing.
48
+ - When a project has files in `.husky`, run every direct script in `.husky` before finishing. Fix any issue they report before finalizing. Never skip, bypass, or silence these scripts.
49
+ - If a required `.junie` or `.husky` command cannot be run because of a missing dependency, unavailable service, credentials, or environment limitation, clearly report the exact command, the failure reason, and the remaining risk in the final response.
50
+ - Avoid editing generated or heavy-output directories such as `node_modules`, `dist`, `coverage`, `.next`, `logs`, and generated stores unless the task explicitly requires it.
51
+
52
+ ### Backend Microservices
53
+
54
+ The `api-*` directories are independent Express/Postgres backend services. Most JavaScript services use CommonJS, Mocha, Supertest, Docker Compose database tests, `@carecard/*` packages, and `sub-apps` controller/router/model patterns. TypeScript services such as `api-contact-us` and `api-template-ts` use Jest or TypeScript tooling and should keep their existing TS style.
55
+
56
+ - Keep service-specific controllers thin. Controllers should read as a clear workflow: parse input, authorize, validate, call domain/model logic, build response, and pass errors to `next`.
57
+ - Extract multiline chunks into descriptively named functions in the appropriate `controllerLib`, `commonLib`, `sub-apps/lib`, model helper, or shared `pkg-*` package.
58
+ - Avoid defining reusable workflow, validation, mapping, response, authorization, parsing, or domain helpers in the same controller/app file where they are immediately used.
59
+ - Prefer straightforward sequencing of named helper calls over deeply nested conditionals.
60
+ - Preserve current behavior unless fixing a clear bug, security issue, or documented contract problem.
61
+ - Keep application setup files such as `app.js`, `app.ts`, `bin/www`, and routers focused on composition and wiring.
62
+ - Use existing Express middleware patterns: `requestContext`, CORS configuration, Helmet, cookie parsing, body-size limits, rate limits where already present, routers, 404 handling, logging middleware, and centralized error handlers.
63
+ - Use structured, actionable logging for important application events, external calls, state transitions, failures, and security-relevant actions.
64
+ - Do not log secrets, tokens, passwords, credentials, personal identifiers, full request payloads, or stack traces in user-facing responses.
65
+ - Keep logs useful for production monitoring. Avoid noisy logs that fire on every trivial branch unless they are request/access logs already established by the service.
66
+
67
+ ### Shared Packages And API Contracts
68
+
69
+ The `pkg-*` directories are reusable CareCard packages. Shared API response, error, authentication, JWT, and validation behavior belongs there when it is common across services.
70
+
71
+ - Prefer `@carecard/common-util`, `@carecard/auth-util`, `@carecard/jwt-read`, and `@carecard/validate` over duplicated local implementations.
72
+ - For API responses and errors, use the standardized `@carecard/common-util` behavior where possible: `requestContext`, `sendResponse`, `createError`, `notFound404`, `appErrorHandler`, error throw helpers, case converters, and `ApiErrorType`.
73
+ - Do not create or maintain duplicated common response/error helpers inside each `api-*` service. If a reusable capability is missing, add it to the correct `pkg-*` package and update callers.
74
+ - Keep service-local response code limited to service-specific mapping or wiring.
75
+ - Preserve the standard response shape expected by the dashboard: `success`, `status`, `statusCode`, `code`, `message`, `data`, `error`, `details`, and `meta`.
76
+ - Include request/correlation context where available through `requestId`, `traceId`, and `meta`.
77
+ - Error responses must be safe for users and useful for debugging without exposing secrets, tokens, credentials, stack traces, or sensitive personal data.
78
+ - Map validation, authentication, authorization, not-found, conflict, bad input, file, and unexpected failures to distinct machine-readable codes.
79
+ - Prefer current direct exports from shared packages over deprecated nested exports. For example, prefer direct `@carecard/jwt-read` function names and direct `@carecard/auth-util` helpers.
80
+
81
+ ### Validation Rules
82
+
83
+ - Keep request-boundary validation close to the API/controller layer.
84
+ - Use `validateWhitelistProperties()` once per validation boundary unless there is a specific documented reason to validate defensively again.
85
+ - Avoid hidden duplicate validation across controller and library layers for the same logical payload.
86
+ - Keep domain/library functions focused on domain behavior and assume validated inputs when called from validated controller paths.
87
+ - If a public/shared library function still needs defensive validation, document why and avoid repeating the exact same validation already done by the caller.
88
+ - Preserve validation behavior for invalid, missing, extra, and valid fields.
89
+ - Pay attention to nested fields, dot-notation paths, camelCase/snake_case conversion, and frontend response transforms.
90
+
91
+ ### Tests
92
+
93
+ - Write or update tests before implementation whenever changing behavior.
94
+ - Testing is mandatory before finalizing code changes. Do not stop after implementation if tests, `.junie`, or `.husky` checks remain unrun.
95
+ - Code coverage must never be lower than the previous commit. When coverage tooling exists, compare against the previous commit or recorded baseline before finalizing, add tests to maintain or improve coverage, and never reduce coverage thresholds to make checks pass.
96
+ - Keep tests readable and domain-specific. Prefer explicit helper names over generic test utilities that hide important behavior.
97
+ - Use existing test frameworks and layouts:
98
+ - JavaScript `api-*`: usually Mocha, Supertest, `test/index.test.js`, and Docker-backed Postgres scripts.
99
+ - TypeScript `api-*`: usually Jest and `tests/index.test.ts`.
100
+ - `pkg-*`: Mocha plus TypeScript type tests where present.
101
+ - `app-dashboard`: Vitest, React Testing Library, mock API tests, and Selenium for end-to-end flows.
102
+ - For database tests, use existing seed, migration, rollback, and cleanup patterns. Keep tests isolated and make cleanup reliable even after failures.
103
+ - Add tests for API success responses, validation errors, auth/authz errors, JWT errors, not-found/conflict cases, and unexpected error handling when those paths change.
104
+ - For frontend changes, test validation, transforms, query/mutation wrappers, components, and user-visible flows at the narrowest practical level first.
105
+ - If any test or repository check fails, fix the issue and rerun the failing command. Only finalize with failing checks when the failure is unrelated to the change or blocked by environment constraints, and document that explicitly.
106
+
107
+ ### Dashboard Frontend
108
+
109
+ `app-dashboard` is a Next.js App Router TypeScript app using MUI, React Query, `next-intl`, and shared CareCard utilities. It consumes `api-auth`, `api-institutions`, `api-contact-us`, and `api-user-profiles` through service modules.
110
+
111
+ - Keep backend URL definitions centralized in `src/services/api.routes.ts`.
112
+ - Keep fetch behavior centralized in `src/services/common/api`, especially `appFetch`, `api.client`, and `parseApiResponse`.
113
+ - Services should return typed app/domain objects or typed form states, not raw fetch responses.
114
+ - Keep validation in `*.validation.ts`, API calls in `*.queries.ts` or mutation helpers, mapping in `*.transform.ts`, and orchestration in `*.service.ts`.
115
+ - Preserve the standardized `ApiResponse` parsing behavior for both current backend responses and legacy/non-standard responses.
116
+ - Do not expose JWTs, session contents, or sensitive backend details in client components or logs.
117
+ - Respect `basePath: '/secure'`, server actions, middleware session renewal, mock API mode, and existing i18n message patterns.
118
+ - Use existing MUI and app component patterns. Do not introduce a new UI framework.
119
+
120
+ ### Dependency And Version Guidance
121
+
122
+ - Keep CareCard package usage consistent with the service being changed.
123
+ - When standardizing response/error behavior, prefer `@carecard/common-util` `3.1.15` because it contains response and error functions aligned with `api-auth`.
124
+ - If package version changes are required, update lockfiles and verify affected services.
125
+ - Avoid broad dependency upgrades as part of feature or refactor work unless the task is specifically about dependencies.
126
+
127
+ ### Security Requirements
128
+
129
+ - Treat authentication, authorization, JWT, password, email confirmation, recovery, file upload, CORS, rate limits, and error response behavior as security-sensitive.
130
+ - Never log or return secrets, tokens, passwords, credentials, private keys, full JWT payloads, or sensitive personal data.
131
+ - Use safe error messages for users and structured details only when they do not reveal sensitive implementation or data.
132
+ - Keep body-size limits, Helmet, CORS allow-lists, and rate-limit behavior intact unless a task explicitly changes them.
133
+ - Document remaining security concerns that require product, infrastructure, or deployment decisions.
134
+
135
+ ## Repository-Specific Instructions
136
+
137
+ ### Non-Negotiable Instructions
138
+
139
+ - Never use TypeScript type `any`. Use specific value, record, validator, option, result, generic, or `unknown` types with narrowing.
140
+ - Always follow this repository's coding style, naming conventions, and CommonJS project structure.
141
+ - Always use Test-Driven Development: add or update the relevant Mocha or type tests before changing behavior.
142
+ - Never suppress errors, linter warnings, TypeScript errors, or failing tests. Handle the underlying issue.
143
+ - Do not add new dependencies unless they are absolutely needed. Ask for confirmation first with the reason and tradeoff.
144
+ - Before finalizing work in this repository, run every script in `.husky/` and fix anything they report.
145
+
146
+ ### Package Shape
147
+
148
+ - Keep `index.js` as the centralized public export surface.
149
+ - Keep TypeScript declarations in `index.d.ts` aligned with every public export in `index.js`.
150
+ - Keep direct validators in `lib/validate.js`.
151
+ - Keep key-based property sanitization in `lib/validateProperties.js`.
152
+ - Keep whitelist, nested-path, casing, flattening, and CareCard bad-input behavior in `lib/validateWhitelistProperties.js`.
153
+ - Preserve the package's CommonJS module style unless the repository is intentionally migrated.
154
+ - Keep the deprecated `validate` namespace export backward-compatible while preferring direct top-level exports in new code.
155
+
156
+ ### Validation Rules
157
+
158
+ - Low-level validators should be deterministic predicate functions that return `true` or `false`.
159
+ - Password failure-message helpers should return `null` for valid input and a user-readable string for invalid input.
160
+ - `validateProperties` should return a new sanitized object and omit unknown or invalid fields without mutating the input.
161
+ - `validateWhitelistProperties` should reject missing or invalid required fields with CareCard `BAD_INPUT` errors through `@carecard/common-util`.
162
+ - Optional whitelist fields should be ignored when absent and rejected when present but invalid.
163
+ - Preserve supported snake_case and camelCase field aliases unless a task explicitly changes the API contract.
164
+ - Preserve nested dot-path handling, the maximum nesting depth, maximum path count, optional snake_case conversion, and flattening behavior.
165
+ - Avoid broad regular expressions or validation changes without focused tests for accepted values, rejected values, length limits, and edge cases.
166
+
167
+ ### Types And API Contracts
168
+
169
+ - Model input and output records, whitelist options, flattened output behavior, validators, and failure-message helpers explicitly in `index.d.ts`.
170
+ - When existing declarations are too loose, improve them with specific types as part of the touched change instead of adding new loose types.
171
+ - Update `test/types.test.ts` whenever public types, exports, options, return values, or validators change.
172
+ - Keep runtime exports, README examples, and type declarations in sync.
173
+
174
+ ### Tests
175
+
176
+ - Use Mocha for runtime tests under `test/`.
177
+ - Use `test/types.test.ts` for TypeScript declaration coverage through `npm run test:types`.
178
+ - Add focused tests for valid input, invalid input, missing fields, optional fields, array handling, nested paths, casing conversion, flattening, and error messages when those areas change.
179
+ - Keep tests deterministic and avoid relying on real external services.
180
+ - Before pushing or finalizing, run `.husky/pre-commit`; it runs lint fixing, formatting, and `npm run test:All`.
@@ -0,0 +1,14 @@
1
+ # Full repository owner
2
+ * @singh-pankaj-k
3
+
4
+ # Repository workflow and ownership metadata
5
+ .github/ @singh-pankaj-k
6
+ README.md @singh-pankaj-k
7
+ package.json @singh-pankaj-k
8
+ package-lock.json @singh-pankaj-k
9
+
10
+ # Package source, declarations, and tests
11
+ index.js @singh-pankaj-k
12
+ index.d.ts @singh-pankaj-k
13
+ lib/ @singh-pankaj-k
14
+ test/ @singh-pankaj-k
@@ -23,6 +23,8 @@ on:
23
23
  push:
24
24
  branches:
25
25
  - 'feature/**'
26
+ - 'feat/**'
27
+ - 'improvement/**'
26
28
  - 'releases/**'
27
29
  - 'release*'
28
30
  - 'hotfix/**'
@@ -30,6 +32,8 @@ on:
30
32
  - 'fix/**'
31
33
  - 'main-*'
32
34
  - 'main-**'
35
+ - 'data**'
36
+ - 'data/**'
33
37
  paths-ignore:
34
38
  - '**.md'
35
39
  workflow_dispatch:
@@ -1,17 +1,54 @@
1
1
  name: CI
2
2
 
3
3
  on:
4
+ workflow_dispatch:
4
5
  push:
6
+ branches:
7
+ - main
8
+ - develop
9
+ - development
10
+ - dev*
11
+ - feature/**
12
+ - feat/**
13
+ - improvement/**
14
+ - releases/**
15
+ - release*
16
+ - hotfix/**
17
+ - iss/**
18
+ - fix/**
19
+ - data**
20
+ - data/**
21
+ paths-ignore:
22
+ - '**.md'
5
23
  pull_request:
24
+ types: [opened, synchronize, reopened, ready_for_review]
25
+ branches:
26
+ - main
27
+ - develop
28
+ - development
29
+ - dev*
30
+ - feature/**
31
+ - feat/**
32
+ - improvement/**
33
+ - releases/**
34
+ - release*
35
+ - hotfix/**
36
+ - iss/**
37
+ - fix/**
38
+ - data**
39
+ - data/**
40
+ paths-ignore:
41
+ - '**.md'
6
42
 
7
43
  jobs:
8
44
  test:
9
45
  runs-on: ubuntu-latest
10
46
 
11
47
  steps:
12
- - uses: actions/checkout@v4
48
+ - name: Checkout repository
49
+ uses: actions/checkout@v4
13
50
 
14
- - name: Use Node.js
51
+ - name: Set up Node.js
15
52
  uses: actions/setup-node@v4
16
53
  with:
17
54
  node-version: '25'
@@ -20,5 +57,8 @@ jobs:
20
57
  - name: Install dependencies
21
58
  run: npm ci
22
59
 
23
- - name: Run tests and coverage
60
+ - name: Run tests
24
61
  run: npm run test:All
62
+
63
+ - name: Run tests with coverage
64
+ run: npm run test:coverage
package/.husky/pre-commit CHANGED
@@ -1,5 +1,3 @@
1
- npm run lint:fix
2
- npm run format
3
-
4
1
  # Run tests
5
- npm run test:All
2
+ npm run test
3
+ npm run test:types
package/index.d.ts CHANGED
@@ -13,18 +13,10 @@ 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. No nested objects remain in the output. Applied
17
- * after snake_case conversion.
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.
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';
28
20
  }
29
21
 
30
22
  /**
@@ -46,11 +38,10 @@ export interface ValidateWhitelistPropertiesOptions {
46
38
  * validated elements (e.g. `{ name: ["First", "Other"] }` is validated like
47
39
  * `{ name: "First" }` and `{ name: "Other" }` individually).
48
40
  * - Optionally converts the resulting keys (including nested keys) to snake_case.
49
- * - Optionally flattens the result after snake_case conversion.
50
41
  *
51
42
  * @param inputObject The input object (e.g. `req.body` or `req.params`).
52
43
  * @param requiredProperties Leaf paths that must be present and valid. Dot-notation supported.
53
- * @param options Optional additional leaf paths plus output transformation flags.
44
+ * @param options Optional list of additional allowed leaf paths and case-conversion flag.
54
45
  */
55
46
  export function validateWhitelistProperties(
56
47
  inputObject: Record<string, any>,
@@ -58,6 +49,24 @@ export function validateWhitelistProperties(
58
49
  options?: ValidateWhitelistPropertiesOptions,
59
50
  ): Promise<Record<string, any>>;
60
51
 
52
+ export const DEFAULT_USER_ROLE_REQUEST_ROLE: 'student';
53
+ export const REQUIRE_SCOPE_WHEN_ROLE_OR_SCOPE_PRESENT: 'whenRoleOrScopePresent';
54
+
55
+ export interface ValidateNewUserRoleRequestOptions {
56
+ defaultRole?: 'student' | undefined;
57
+ requireScope?: boolean | typeof REQUIRE_SCOPE_WHEN_ROLE_OR_SCOPE_PRESENT;
58
+ }
59
+
60
+ /**
61
+ * Normalizes and validates a carecard.new_user_role_request payload.
62
+ * Only student, intern, and volunteer are accepted. When scope is required,
63
+ * both institution_id and campus_id must be provided.
64
+ */
65
+ export function validateNewUserRoleRequestObject(
66
+ roleRequest?: Record<string, any>,
67
+ options?: ValidateNewUserRoleRequestOptions,
68
+ ): Record<string, any>;
69
+
61
70
  /** Checks if the string is a valid image URL format. */
62
71
  export function isImageUrl(imageUrl: any): boolean;
63
72
  /** Checks if the value is an integer. */
@@ -106,8 +115,14 @@ export function isBoolValue(inputValue: any): boolean;
106
115
  export function isPostalCodeString(inputString: any): boolean;
107
116
  /** Checks if the string contains only allowed "safe" characters. */
108
117
  export function isSafeString(str: any): boolean;
118
+ /** Checks if the value is non-empty text up to the supported maximum length. */
119
+ export function isTextString(str: any): boolean;
109
120
  /** Checks if a string exists within a given array of strings (case-insensitive). */
110
121
  export function isInStringArray(StringArray: string[], inputString: any): boolean;
122
+ /** Checks if the string is one of the supported user role request statuses. */
123
+ export function isUserRoleRequestStatusString(inputString: any): boolean;
124
+ /** Checks if the string is a supported new user role request role. */
125
+ export function isUserRoleRequestRoleString(inputString: any): boolean;
111
126
  /** Checks if the string is a valid country code (e.g., +1). */
112
127
  export function isCountryCodeString(str: any): boolean;
113
128
  /** Checks if the string is a valid domain name. */
@@ -151,7 +166,10 @@ export const validate: {
151
166
  isBoolValue: typeof isBoolValue;
152
167
  isPostalCodeString: typeof isPostalCodeString;
153
168
  isSafeString: typeof isSafeString;
169
+ isTextString: typeof isTextString;
154
170
  isInStringArray: typeof isInStringArray;
171
+ isUserRoleRequestStatusString: typeof isUserRoleRequestStatusString;
172
+ isUserRoleRequestRoleString: typeof isUserRoleRequestRoleString;
155
173
  isCountryCodeString: typeof isCountryCodeString;
156
174
  isValidDomainName: typeof isValidDomainName;
157
175
  isValidTimestampzString: typeof isValidTimestampzString;
package/index.js CHANGED
@@ -1,11 +1,13 @@
1
1
  const validate = require('./lib/validate');
2
2
  const validateProperties = require('./lib/validateProperties');
3
3
  const validateWhitelistProperties = require('./lib/validateWhitelistProperties');
4
+ const validateNewUserRoleRequest = require('./lib/validateNewUserRoleRequest');
4
5
 
5
6
  module.exports = {
6
7
  validate,
7
8
  validateProperties,
8
9
  validateWhitelistProperties,
10
+ ...validateNewUserRoleRequest,
9
11
  ...validate,
10
12
  ...validateProperties,
11
13
  };
package/lib/validate.js CHANGED
@@ -156,6 +156,10 @@ const isSafeString = str => {
156
156
  return /^[\da-zA-Z-_.,#*'()[\]: ]+$/.test(str);
157
157
  };
158
158
 
159
+ const isTextString = str => {
160
+ return typeof str === 'string' && str.length > 0 && str.length <= 10000;
161
+ };
162
+
159
163
  const isInStringArray = (StringArray, inputString) => {
160
164
  if (isNameString(inputString)) {
161
165
  return StringArray.includes(inputString.toLowerCase().trim());
@@ -164,6 +168,16 @@ const isInStringArray = (StringArray, inputString) => {
164
168
  return false;
165
169
  };
166
170
 
171
+ const isUserRoleRequestStatusString = inputString => {
172
+ const statuses = ['pending', 'approved', 'rejected', 'cancelled', 'expired', 'hidden', 'on_hold', 'in_progress', 'info_needed'];
173
+ return isInStringArray(statuses, inputString);
174
+ };
175
+
176
+ const isUserRoleRequestRoleString = inputString => {
177
+ const roles = ['student', 'intern', 'volunteer'];
178
+ return typeof inputString === 'string' && roles.includes(inputString.trim().toLowerCase());
179
+ };
180
+
167
181
  const isCountryCodeString = str => {
168
182
  if (typeof str !== 'string' || str.length === 0 || str.length > 4) return false;
169
183
 
@@ -231,7 +245,10 @@ module.exports = {
231
245
  isBoolValue,
232
246
  isPostalCodeString,
233
247
  isSafeString,
248
+ isTextString,
234
249
  isInStringArray,
250
+ isUserRoleRequestStatusString,
251
+ isUserRoleRequestRoleString,
235
252
  isCountryCodeString,
236
253
  isValidDomainName,
237
254
  isValidTimestampzString,
@@ -0,0 +1,79 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ error: { throwBadInputError },
5
+ } = require('@carecard/common-util');
6
+ const { isUserRoleRequestRoleString } = require('./validate');
7
+
8
+ const DEFAULT_USER_ROLE_REQUEST_ROLE = 'student';
9
+ const REQUIRE_SCOPE_WHEN_ROLE_OR_SCOPE_PRESENT = 'whenRoleOrScopePresent';
10
+
11
+ function validateNewUserRoleRequestObject(roleRequest = {}, options = {}) {
12
+ const normalized = normalizeNewUserRoleRequestObject(roleRequest);
13
+ const defaultRole = Object.prototype.hasOwnProperty.call(options, 'defaultRole') ? options.defaultRole : DEFAULT_USER_ROLE_REQUEST_ROLE;
14
+ const requireScope = Object.prototype.hasOwnProperty.call(options, 'requireScope') ? options.requireScope : true;
15
+
16
+ if (normalized.role_name === undefined && defaultRole !== undefined) {
17
+ normalized.role_name = defaultRole;
18
+ }
19
+
20
+ if (normalized.role_name !== undefined && !isUserRoleRequestRoleString(normalized.role_name)) {
21
+ throwBadInputError({
22
+ userMessage: 'Invalid property: role.role',
23
+ details: { role: 'Role requests are limited to student, intern, or volunteer' },
24
+ });
25
+ }
26
+
27
+ if (shouldRequireScope(normalized, requireScope)) {
28
+ requireNewUserRoleRequestScope(normalized);
29
+ }
30
+
31
+ return normalized;
32
+ }
33
+
34
+ function normalizeNewUserRoleRequestObject(roleRequest) {
35
+ const normalized = { ...roleRequest };
36
+
37
+ assignAlias(normalized, 'role_name', 'roleName');
38
+ assignAlias(normalized, 'role_name', 'role');
39
+ assignAlias(normalized, 'institution_id', 'institutionId');
40
+ assignAlias(normalized, 'campus_id', 'campusId');
41
+ assignAlias(normalized, 'program_id', 'programId');
42
+
43
+ delete normalized.roleName;
44
+ delete normalized.role;
45
+ delete normalized.institutionId;
46
+ delete normalized.campusId;
47
+ delete normalized.programId;
48
+
49
+ return normalized;
50
+ }
51
+
52
+ function assignAlias(target, canonicalKey, aliasKey) {
53
+ if (target[canonicalKey] === undefined && target[aliasKey] !== undefined) {
54
+ target[canonicalKey] = target[aliasKey];
55
+ }
56
+ }
57
+
58
+ function shouldRequireScope(roleRequest, requireScope) {
59
+ if (requireScope === true) return true;
60
+ if (requireScope !== REQUIRE_SCOPE_WHEN_ROLE_OR_SCOPE_PRESENT) return false;
61
+
62
+ return roleRequest.role_name !== undefined || roleRequest.institution_id !== undefined || roleRequest.campus_id !== undefined;
63
+ }
64
+
65
+ function requireNewUserRoleRequestScope(roleRequest) {
66
+ if (!roleRequest.institution_id) {
67
+ throwBadInputError({ userMessage: 'Missing property: role.institutionId' });
68
+ }
69
+
70
+ if (!roleRequest.campus_id) {
71
+ throwBadInputError({ userMessage: 'Missing property: role.campusId' });
72
+ }
73
+ }
74
+
75
+ module.exports = {
76
+ DEFAULT_USER_ROLE_REQUEST_ROLE,
77
+ REQUIRE_SCOPE_WHEN_ROLE_OR_SCOPE_PRESENT,
78
+ validateNewUserRoleRequestObject,
79
+ };
@@ -20,6 +20,9 @@ const {
20
20
  isValidUrl,
21
21
  isValidArrayOfStrings,
22
22
  isStreetString,
23
+ isTextString,
24
+ isUserRoleRequestStatusString,
25
+ isUserRoleRequestRoleString,
23
26
  } = require('./validate');
24
27
 
25
28
  function validateProperties(obj = {}) {
@@ -63,6 +66,8 @@ function validateProperties(obj = {}) {
63
66
  case 'entityType':
64
67
  case 'action_type':
65
68
  case 'actionType':
69
+ case 'approved_by_role':
70
+ case 'approvedByRole':
66
71
  case 'city':
67
72
  case 'state':
68
73
  case 'country':
@@ -164,10 +169,40 @@ function validateProperties(obj = {}) {
164
169
  case 'changedBy':
165
170
  case 'request_id':
166
171
  case 'requestId':
172
+ case 'approved_by_user_id':
173
+ case 'approvedByUserId':
167
174
  if (isValidUuidString(value)) {
168
175
  returnObj[key] = value;
169
176
  }
170
177
  break;
178
+ case 'requested_by_name':
179
+ case 'requestedByName':
180
+ case 'requested_by_email':
181
+ case 'requestedByEmail':
182
+ case 'requested_by_phone':
183
+ case 'requestedByPhone':
184
+ case 'approved_by_name':
185
+ case 'approvedByName':
186
+ case 'approved_by_email':
187
+ case 'approvedByEmail':
188
+ case 'approved_by_phone':
189
+ case 'approvedByPhone':
190
+ if (isTextString(value)) {
191
+ returnObj[key] = value;
192
+ }
193
+ break;
194
+ case 'approved_status':
195
+ case 'approvedStatus':
196
+ if (isUserRoleRequestStatusString(value)) {
197
+ returnObj[key] = value;
198
+ }
199
+ break;
200
+ case 'user_role_request_role':
201
+ case 'userRoleRequestRole':
202
+ if (isUserRoleRequestRoleString(value)) {
203
+ returnObj[key] = value;
204
+ }
205
+ break;
171
206
  case 'period':
172
207
  if (isCharactersString(value)) {
173
208
  returnObj[key] = value;
@@ -229,10 +264,14 @@ function validateProperties(obj = {}) {
229
264
  break;
230
265
  case 'expires_at':
231
266
  case 'expiresAt':
267
+ case 'starts_at':
268
+ case 'startsAt':
232
269
  case 'start_time':
233
270
  case 'startTime':
234
271
  case 'end_time':
235
272
  case 'endTime':
273
+ case 'approved_at':
274
+ case 'approvedAt':
236
275
  if (isValidTimestampzString(value) || isValidTimestampString(value)) {
237
276
  returnObj[key] = value;
238
277
  }
@@ -16,8 +16,6 @@ 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']);
21
19
 
22
20
  /**
23
21
  * Returns true if the segment contains a mix of snake_case (underscore) and
@@ -163,35 +161,6 @@ function flattenObject(obj, prefix = '', out = {}) {
163
161
  return out;
164
162
  }
165
163
 
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
-
195
164
  /**
196
165
  * Validates and transforms whitelisted properties from an input object.
197
166
  *
@@ -212,11 +181,8 @@ function flattenObjectByLeafKey(obj, out = {}, depthByKey = {}, depth = 1) {
212
181
  * element passes validation, and the returned value is an array of the
213
182
  * validated elements (in the same order).
214
183
  * 5. Optionally converts all keys (including nested) to snake_case.
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.
184
+ * 6. Optionally flattens the result so every leaf is a top-level key,
185
+ * joined by `.` (`flattenOutput`). Applied after snake_case conversion.
220
186
  *
221
187
  * @param {Object} inputObject - The input object (e.g., req.body / req.params).
222
188
  * @param {Array<string>} [requiredProperties=[]] - Leaf paths that MUST be present and valid.
@@ -224,26 +190,17 @@ function flattenObjectByLeafKey(obj, out = {}, depthByKey = {}, depth = 1) {
224
190
  * @param {Array<string>} [options.optionalProperties=[]] - Leaf paths allowed but not required.
225
191
  * @param {boolean} [options.convertToSnakeCase=false] - Whether to convert keys to snake_case.
226
192
  * @param {boolean} [options.flattenOutput=false] - Whether to flatten the result so that
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.
193
+ * every leaf is a top-level key (joined by `.`), with no nested objects in the output.
230
194
  * @returns {Promise<Object>} Resolves with the validated (and possibly transformed) object.
231
195
  */
232
196
  function validateWhitelistProperties(
233
197
  inputObject,
234
198
  requiredProperties = [],
235
- options = { optionalProperties: [], convertToSnakeCase: false, flattenOutput: false, flattenKeyStyle: DEFAULT_FLATTEN_KEY_STYLE },
199
+ options = { optionalProperties: [], convertToSnakeCase: false, flattenOutput: false },
236
200
  ) {
237
201
  const optionalProperties = (options && options.optionalProperties) || [];
238
202
  const convertToSnakeCase = !!(options && options.convertToSnakeCase);
239
203
  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
- }
247
204
 
248
205
  // Cap the total number of paths to validate per call.
249
206
  const totalKeys = (requiredProperties ? requiredProperties.length : 0) + optionalProperties.length;
@@ -314,9 +271,9 @@ function validateWhitelistProperties(
314
271
  validatedObject = keysToSnakeCase(validatedObject);
315
272
  }
316
273
 
317
- // 6. Optional flattening.
274
+ // 6. Optional flattening: produce a flat object with dot-joined keys.
318
275
  if (flattenOutput) {
319
- validatedObject = flattenKeyStyle === 'leaf' ? flattenObjectByLeafKey(validatedObject) : flattenObject(validatedObject);
276
+ validatedObject = flattenObject(validatedObject);
320
277
  }
321
278
 
322
279
  return Promise.resolve(validatedObject);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carecard/validate",
3
- "version": "3.1.22",
3
+ "version": "3.1.24",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/CareCard-ca/pkg-validate.git"
@@ -12,7 +12,7 @@
12
12
  "test": "mocha --recursive",
13
13
  "test:types": "tsc --noEmit && echo \"\\n ✔ Type tests passed: tsc --noEmit reported 0 errors across index.d.ts and test/**/*.ts\\n\"",
14
14
  "test:coverage": "tsc --noEmit && export NODE_ENV=test && nyc mocha --recursive",
15
- "test:All": "npm run test && npm run test:types",
15
+ "test:All": "npm run test:coverage && npm run test:types",
16
16
  "format": "prettier --write .",
17
17
  "format:check": "prettier --check .",
18
18
  "prepare": "husky",
@@ -39,5 +39,17 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@carecard/common-util": "^3.1.13"
42
+ },
43
+ "nyc": {
44
+ "all": true,
45
+ "include": [
46
+ "index.js",
47
+ "lib/**/*.js"
48
+ ],
49
+ "check-coverage": true,
50
+ "branches": 100,
51
+ "functions": 100,
52
+ "lines": 100,
53
+ "statements": 100
42
54
  }
43
55
  }