diff --git a/.github/actions/install/action.yaml b/.github/actions/install/action.yaml index 9f259ac092af..5129d9832fc9 100644 --- a/.github/actions/install/action.yaml +++ b/.github/actions/install/action.yaml @@ -15,6 +15,15 @@ runs: registry-url: "https://registry.npmjs.org" cache: pnpm + - name: ⎔ Cache turbo build + uses: actions/cache@v4 + with: + path: .turbo + key: ${{ runner.os }}-turbo-${{ github.job }}-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-turbo-${{ github.job }}- + ${{ runner.os }}-turbo- + # Ensure npm 11.5.1 or later is installed - name: ⎔ Update npm shell: bash diff --git a/.github/workflows/check-devin-pr-assignee.yml b/.github/workflows/check-devin-pr-assignee.yml index 4feb838ab193..c32041f0079d 100644 --- a/.github/workflows/check-devin-pr-assignee.yml +++ b/.github/workflows/check-devin-pr-assignee.yml @@ -7,6 +7,9 @@ on: permissions: pull-requests: write +env: + DO_NOT_TRACK: "1" + jobs: check-assignee: runs-on: ubuntu-latest diff --git a/.github/workflows/ci-dynamic-snippets.yml b/.github/workflows/ci-dynamic-snippets.yml index b452ff75c7b7..71c5850b6a8c 100644 --- a/.github/workflows/ci-dynamic-snippets.yml +++ b/.github/workflows/ci-dynamic-snippets.yml @@ -16,6 +16,7 @@ permissions: contents: read env: + DO_NOT_TRACK: "1" TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: "buildwithfern" TURBO_REMOTE_CACHE_TIMEOUT: 60 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9b975a7cf36a..d1f4d5cc1b21 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,7 @@ concurrency: cancel-in-progress: true env: + DO_NOT_TRACK: "1" TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: "buildwithfern" TURBO_REMOTE_CACHE_TIMEOUT: 60 @@ -186,19 +187,29 @@ jobs: - name: Install protoc-gen-openapi run: go install github.com/fern-api/protoc-gen-openapi/cmd/protoc-gen-openapi@latest + - name: Build CLI (dev) + timeout-minutes: 5 + run: pnpm turbo run dist:cli:dev --filter=@fern-api/cli --filter=@fern-api/cli-v2 + + - name: Build seed CLI + timeout-minutes: 5 + run: pnpm seed:build + - name: Run ETE tests if: ${{ !github.event.inputs.update_snapshots }} + timeout-minutes: 10 env: FERN_ORG_TOKEN_DEV: ${{ secrets.FERN_ORG_TOKEN_DEV }} run: | - FERN_TOKEN=${{ secrets.FERN_ORG_TOKEN_DEV }} pnpm test:ete + FERN_TOKEN=${{ secrets.FERN_ORG_TOKEN_DEV }} pnpm --filter @fern-api/ete-tests test - name: Run ETE tests with snapshot updates if: ${{ github.event.inputs.update_snapshots == 'true' }} + timeout-minutes: 10 env: FERN_ORG_TOKEN_DEV: ${{ secrets.FERN_ORG_TOKEN_DEV }} run: | - FERN_TOKEN=${{ secrets.FERN_ORG_TOKEN_DEV }} pnpm test:ete:update + FERN_TOKEN=${{ secrets.FERN_ORG_TOKEN_DEV }} pnpm --filter @fern-api/ete-tests test -- -u - name: Commit and push updated snapshots if: ${{ github.event.inputs.update_snapshots == 'true' }} diff --git a/.github/workflows/cli-release.yml b/.github/workflows/cli-release.yml index fb7388a57bc9..6f72346802a3 100644 --- a/.github/workflows/cli-release.yml +++ b/.github/workflows/cli-release.yml @@ -13,6 +13,7 @@ permissions: pull-requests: write env: + DO_NOT_TRACK: "1" GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} jobs: diff --git a/.github/workflows/definitions-validation.yml b/.github/workflows/definitions-validation.yml index a332309b7a2d..2b86df74d206 100644 --- a/.github/workflows/definitions-validation.yml +++ b/.github/workflows/definitions-validation.yml @@ -8,6 +8,7 @@ on: - main env: + DO_NOT_TRACK: "1" TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: "buildwithfern" TURBO_REMOTE_CACHE_TIMEOUT: 60 diff --git a/.github/workflows/dependabot.yml b/.github/workflows/dependabot.yml index 4f4664fc3717..44be0336ff24 100644 --- a/.github/workflows/dependabot.yml +++ b/.github/workflows/dependabot.yml @@ -10,6 +10,9 @@ permissions: contents: write pull-requests: write +env: + DO_NOT_TRACK: "1" + jobs: auto-merge: runs-on: ubuntu-latest diff --git a/.github/workflows/fix-lint.yml b/.github/workflows/fix-lint.yml index de84c6f94bf4..70acb4864c9f 100644 --- a/.github/workflows/fix-lint.yml +++ b/.github/workflows/fix-lint.yml @@ -11,6 +11,7 @@ concurrency: cancel-in-progress: true env: + DO_NOT_TRACK: "1" TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: "buildwithfern" TURBO_REMOTE_CACHE_TIMEOUT: 60 diff --git a/.github/workflows/go-generator.yml b/.github/workflows/go-generator.yml index ea290455421e..5924895f4d8a 100644 --- a/.github/workflows/go-generator.yml +++ b/.github/workflows/go-generator.yml @@ -8,6 +8,9 @@ on: - main workflow_call: +env: + DO_NOT_TRACK: "1" + jobs: compile: runs-on: ubuntu-latest diff --git a/.github/workflows/ir-release.yml b/.github/workflows/ir-release.yml index 03b7d69e73c4..889478331a69 100644 --- a/.github/workflows/ir-release.yml +++ b/.github/workflows/ir-release.yml @@ -8,6 +8,9 @@ on: paths: - "packages/ir-sdk/fern/apis/ir-types-latest/VERSION" +env: + DO_NOT_TRACK: "1" + jobs: node: runs-on: ubuntu-latest diff --git a/.github/workflows/ir-validation.yml b/.github/workflows/ir-validation.yml index 0472ac7a95ac..132734520c7e 100644 --- a/.github/workflows/ir-validation.yml +++ b/.github/workflows/ir-validation.yml @@ -13,6 +13,9 @@ on: branches: - main +env: + DO_NOT_TRACK: "1" + jobs: run: runs-on: ubuntu-latest diff --git a/.github/workflows/java-generator.yml b/.github/workflows/java-generator.yml index 228ba376e34e..5a07634fd576 100644 --- a/.github/workflows/java-generator.yml +++ b/.github/workflows/java-generator.yml @@ -8,6 +8,9 @@ on: - main workflow_call: +env: + DO_NOT_TRACK: "1" + jobs: compile: runs-on: ubuntu-latest diff --git a/.github/workflows/lint-pr-title.yml b/.github/workflows/lint-pr-title.yml index 91de1a979cae..6137f56900bb 100644 --- a/.github/workflows/lint-pr-title.yml +++ b/.github/workflows/lint-pr-title.yml @@ -11,6 +11,9 @@ on: permissions: pull-requests: read +env: + DO_NOT_TRACK: "1" + jobs: run: name: Lint PR title diff --git a/.github/workflows/nightly-seed-metrics.yml b/.github/workflows/nightly-seed-metrics.yml index bdca34341949..57312d54d4a9 100644 --- a/.github/workflows/nightly-seed-metrics.yml +++ b/.github/workflows/nightly-seed-metrics.yml @@ -12,6 +12,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + DO_NOT_TRACK: "1" + jobs: trigger-seed-snapshot-tests: runs-on: ubuntu-latest diff --git a/.github/workflows/openapi-ir-validation.yml b/.github/workflows/openapi-ir-validation.yml index 68b7fc669327..d8ef9a01e330 100644 --- a/.github/workflows/openapi-ir-validation.yml +++ b/.github/workflows/openapi-ir-validation.yml @@ -13,6 +13,9 @@ on: branches: - main +env: + DO_NOT_TRACK: "1" + jobs: run: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml index 542c30fb16e8..a38be8367977 100644 --- a/.github/workflows/publish-cli.yml +++ b/.github/workflows/publish-cli.yml @@ -24,6 +24,7 @@ permissions: id-token: write # Required for OIDC env: + DO_NOT_TRACK: "1" TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: "buildwithfern" TURBO_REMOTE_CACHE_TIMEOUT: 60 diff --git a/.github/workflows/publish-docs-parsers-sdk.yml b/.github/workflows/publish-docs-parsers-sdk.yml index 10f991f4a439..1941a7b62231 100644 --- a/.github/workflows/publish-docs-parsers-sdk.yml +++ b/.github/workflows/publish-docs-parsers-sdk.yml @@ -3,6 +3,9 @@ name: Publish @fern-fern/docs-parsers-fern-definition on: workflow_dispatch: +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-generator-cli.yml b/.github/workflows/publish-generator-cli.yml index 1d32ac95d291..9b862cfcb282 100644 --- a/.github/workflows/publish-generator-cli.yml +++ b/.github/workflows/publish-generator-cli.yml @@ -8,6 +8,7 @@ on: - "packages/generator-cli/versions.yml" env: + DO_NOT_TRACK: "1" PACKAGE_NAME: "@fern-api/generator-cli" TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: "buildwithfern" diff --git a/.github/workflows/publish-generator-csharp-model.yml b/.github/workflows/publish-generator-csharp-model.yml index 06ee69b6db4e..e21453348bb4 100644 --- a/.github/workflows/publish-generator-csharp-model.yml +++ b/.github/workflows/publish-generator-csharp-model.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-generator-csharp-sdk.yml b/.github/workflows/publish-generator-csharp-sdk.yml index 0828c30638a4..57427a0ae99b 100644 --- a/.github/workflows/publish-generator-csharp-sdk.yml +++ b/.github/workflows/publish-generator-csharp-sdk.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-generator-go-model.yml b/.github/workflows/publish-generator-go-model.yml index 96d3369499b7..74ac28bfcc8c 100644 --- a/.github/workflows/publish-generator-go-model.yml +++ b/.github/workflows/publish-generator-go-model.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-generator-go-sdk.yml b/.github/workflows/publish-generator-go-sdk.yml index 94c7d61ced19..43ac0d5b19a6 100644 --- a/.github/workflows/publish-generator-go-sdk.yml +++ b/.github/workflows/publish-generator-go-sdk.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-generator-java-model.yml b/.github/workflows/publish-generator-java-model.yml index 54619a061ac2..d63e58d5a430 100644 --- a/.github/workflows/publish-generator-java-model.yml +++ b/.github/workflows/publish-generator-java-model.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-generator-java-sdk.yml b/.github/workflows/publish-generator-java-sdk.yml index 0bdd4d39fddb..79823aa81317 100644 --- a/.github/workflows/publish-generator-java-sdk.yml +++ b/.github/workflows/publish-generator-java-sdk.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: permissions: diff --git a/.github/workflows/publish-generator-java-spring.yml b/.github/workflows/publish-generator-java-spring.yml index bc64f8ffd2f2..36066856ff68 100644 --- a/.github/workflows/publish-generator-java-spring.yml +++ b/.github/workflows/publish-generator-java-spring.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-generator-migrations.yml b/.github/workflows/publish-generator-migrations.yml index 1221ea2bfb10..8276409395a3 100644 --- a/.github/workflows/publish-generator-migrations.yml +++ b/.github/workflows/publish-generator-migrations.yml @@ -15,6 +15,7 @@ on: type: string env: + DO_NOT_TRACK: "1" PACKAGE_NAME: "@fern-api/generator-migrations" TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: "buildwithfern" diff --git a/.github/workflows/publish-generator-openapi.yml b/.github/workflows/publish-generator-openapi.yml index 23d8eb909078..c038cb930059 100644 --- a/.github/workflows/publish-generator-openapi.yml +++ b/.github/workflows/publish-generator-openapi.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-generator-php-model.yml b/.github/workflows/publish-generator-php-model.yml index e1021340bed7..1394015a2614 100644 --- a/.github/workflows/publish-generator-php-model.yml +++ b/.github/workflows/publish-generator-php-model.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-generator-php-sdk.yml b/.github/workflows/publish-generator-php-sdk.yml index 73f81ed4bae9..b884b99e2426 100644 --- a/.github/workflows/publish-generator-php-sdk.yml +++ b/.github/workflows/publish-generator-php-sdk.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-generator-postman.yml b/.github/workflows/publish-generator-postman.yml index ba18efd01a84..db313e78bdc9 100644 --- a/.github/workflows/publish-generator-postman.yml +++ b/.github/workflows/publish-generator-postman.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-generator-python-fastapi.yml b/.github/workflows/publish-generator-python-fastapi.yml index e17d5056b446..e7f34b197373 100644 --- a/.github/workflows/publish-generator-python-fastapi.yml +++ b/.github/workflows/publish-generator-python-fastapi.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-generator-python-pydantic.yml b/.github/workflows/publish-generator-python-pydantic.yml index 23cd691da28a..48250b63a507 100644 --- a/.github/workflows/publish-generator-python-pydantic.yml +++ b/.github/workflows/publish-generator-python-pydantic.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-generator-python-sdk.yml b/.github/workflows/publish-generator-python-sdk.yml index 27520d73b8c3..f8c4ce650999 100644 --- a/.github/workflows/publish-generator-python-sdk.yml +++ b/.github/workflows/publish-generator-python-sdk.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-generator-ruby-sdk.yml b/.github/workflows/publish-generator-ruby-sdk.yml index 2513530639ad..2ef322466b92 100644 --- a/.github/workflows/publish-generator-ruby-sdk.yml +++ b/.github/workflows/publish-generator-ruby-sdk.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-generator-rust-model.yml b/.github/workflows/publish-generator-rust-model.yml index 73d3ed021ee7..26390dcc70f3 100644 --- a/.github/workflows/publish-generator-rust-model.yml +++ b/.github/workflows/publish-generator-rust-model.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-generator-rust-sdk.yml b/.github/workflows/publish-generator-rust-sdk.yml index ddb83a269698..d87566290c1b 100644 --- a/.github/workflows/publish-generator-rust-sdk.yml +++ b/.github/workflows/publish-generator-rust-sdk.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-generator-swift-sdk.yml b/.github/workflows/publish-generator-swift-sdk.yml index fb653c4f0366..e45eb59224ec 100644 --- a/.github/workflows/publish-generator-swift-sdk.yml +++ b/.github/workflows/publish-generator-swift-sdk.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-generator-ts-express.yml b/.github/workflows/publish-generator-ts-express.yml index 3c5d504819cd..dd68848f56ad 100644 --- a/.github/workflows/publish-generator-ts-express.yml +++ b/.github/workflows/publish-generator-ts-express.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-generator-ts-sdk.yml b/.github/workflows/publish-generator-ts-sdk.yml index c2427474cb3e..a467102064ee 100644 --- a/.github/workflows/publish-generator-ts-sdk.yml +++ b/.github/workflows/publish-generator-ts-sdk.yml @@ -18,6 +18,9 @@ permissions: # Required for OIDC id-token: write contents: read +env: + DO_NOT_TRACK: "1" + jobs: publish: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-python-sdk.yml b/.github/workflows/publish-python-sdk.yml index cdea5c59edd7..5dc4a2c98ba6 100644 --- a/.github/workflows/publish-python-sdk.yml +++ b/.github/workflows/publish-python-sdk.yml @@ -8,6 +8,9 @@ on: required: true type: string +env: + DO_NOT_TRACK: "1" + jobs: release: runs-on: ubuntu-latest diff --git a/.github/workflows/publish-snippets-core.yml b/.github/workflows/publish-snippets-core.yml index fe56188286e6..2d50fcb9da6c 100644 --- a/.github/workflows/publish-snippets-core.yml +++ b/.github/workflows/publish-snippets-core.yml @@ -9,6 +9,7 @@ on: type: string env: + DO_NOT_TRACK: "1" PACKAGE_NAME: "@fern-api/snippets-core" GITHUB_TOKEN: ${{ secrets.FERN_GITHUB_TOKEN }} diff --git a/.github/workflows/publish-typescript-sdk.yml b/.github/workflows/publish-typescript-sdk.yml index 492e5a9da372..0dbbcaeccde0 100644 --- a/.github/workflows/publish-typescript-sdk.yml +++ b/.github/workflows/publish-typescript-sdk.yml @@ -8,6 +8,9 @@ on: required: true type: string +env: + DO_NOT_TRACK: "1" + jobs: release: runs-on: ubuntu-latest diff --git a/.github/workflows/python-generator.yml b/.github/workflows/python-generator.yml index cf22f789f00f..762a25b6748b 100644 --- a/.github/workflows/python-generator.yml +++ b/.github/workflows/python-generator.yml @@ -8,6 +8,9 @@ on: - main workflow_call: +env: + DO_NOT_TRACK: "1" + jobs: compile: runs-on: ubuntu-22.04 diff --git a/.github/workflows/rerun.yml b/.github/workflows/rerun.yml index d9b6f4ccff8e..ad8c256af209 100644 --- a/.github/workflows/rerun.yml +++ b/.github/workflows/rerun.yml @@ -5,6 +5,9 @@ on: inputs: run_id: required: true +env: + DO_NOT_TRACK: "1" + jobs: rerun: runs-on: ubuntu-latest diff --git a/.github/workflows/sdk-ete-tests.yml b/.github/workflows/sdk-ete-tests.yml index 596aeb317913..ee39c90f14f8 100644 --- a/.github/workflows/sdk-ete-tests.yml +++ b/.github/workflows/sdk-ete-tests.yml @@ -145,6 +145,7 @@ on: - 'generators/go/**' env: + DO_NOT_TRACK: "1" TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: "buildwithfern" TURBO_REMOTE_CACHE_TIMEOUT: 60 diff --git a/.github/workflows/security-scanning-and-remediation.yml b/.github/workflows/security-scanning-and-remediation.yml index c8f51bf5b4ed..b96b2624778d 100644 --- a/.github/workflows/security-scanning-and-remediation.yml +++ b/.github/workflows/security-scanning-and-remediation.yml @@ -31,6 +31,7 @@ on: type: boolean env: + DO_NOT_TRACK: "1" TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: "buildwithfern" TURBO_REMOTE_CACHE_TIMEOUT: 60 diff --git a/.github/workflows/seed-dockers.yml b/.github/workflows/seed-dockers.yml index 1048ae4d5ebd..2d3c02d5d89b 100644 --- a/.github/workflows/seed-dockers.yml +++ b/.github/workflows/seed-dockers.yml @@ -40,6 +40,7 @@ concurrency: cancel-in-progress: true env: + DO_NOT_TRACK: "1" DOCKER_BUILDKIT: 1 jobs: diff --git a/.github/workflows/seed-pr-comment.yml b/.github/workflows/seed-pr-comment.yml index ce516f3f0cb9..63c630599bb9 100644 --- a/.github/workflows/seed-pr-comment.yml +++ b/.github/workflows/seed-pr-comment.yml @@ -16,6 +16,9 @@ concurrency: group: seed-pr-comment-${{ github.event.pull_request.number || github.event.issue.number }} cancel-in-progress: true +env: + DO_NOT_TRACK: "1" + jobs: post-seed-selector: name: Post Seed Selector Comment diff --git a/.github/workflows/seed.yml b/.github/workflows/seed.yml index 7bdd6a6815be..1a58d79c9e52 100644 --- a/.github/workflows/seed.yml +++ b/.github/workflows/seed.yml @@ -20,6 +20,7 @@ concurrency: cancel-in-progress: true env: + DO_NOT_TRACK: "1" TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: "buildwithfern" TURBO_REMOTE_CACHE_TIMEOUT: 60 diff --git a/.github/workflows/stale-bot.yml b/.github/workflows/stale-bot.yml index b129f589cd0e..db94b4c54698 100644 --- a/.github/workflows/stale-bot.yml +++ b/.github/workflows/stale-bot.yml @@ -8,6 +8,9 @@ permissions: pull-requests: write contents: write +env: + DO_NOT_TRACK: "1" + jobs: stale-prs: runs-on: ubuntu-latest diff --git a/.github/workflows/test-definitions.yml b/.github/workflows/test-definitions.yml index 013e0b1bcefb..e6c26e9c7e53 100644 --- a/.github/workflows/test-definitions.yml +++ b/.github/workflows/test-definitions.yml @@ -22,6 +22,7 @@ concurrency: cancel-in-progress: true env: + DO_NOT_TRACK: "1" TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: "buildwithfern" TURBO_REMOTE_CACHE_TIMEOUT: 60 diff --git a/.github/workflows/test-remote-vs-local-generation-parity.yml b/.github/workflows/test-remote-vs-local-generation-parity.yml index cfdf206d9b38..d759df6f1e95 100644 --- a/.github/workflows/test-remote-vs-local-generation-parity.yml +++ b/.github/workflows/test-remote-vs-local-generation-parity.yml @@ -32,6 +32,7 @@ concurrency: cancel-in-progress: false env: + DO_NOT_TRACK: "1" TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: "buildwithfern" TURBO_REMOTE_CACHE_TIMEOUT: 60 diff --git a/.github/workflows/typescript-generator.yml b/.github/workflows/typescript-generator.yml index 6f9e5bdb84a7..0bb38af774d5 100644 --- a/.github/workflows/typescript-generator.yml +++ b/.github/workflows/typescript-generator.yml @@ -8,6 +8,9 @@ on: required: true type: string +env: + DO_NOT_TRACK: "1" + jobs: release: runs-on: ubuntu-latest diff --git a/.github/workflows/update-seed.yml b/.github/workflows/update-seed.yml index 76e253c15e2b..36face96bbfc 100644 --- a/.github/workflows/update-seed.yml +++ b/.github/workflows/update-seed.yml @@ -50,6 +50,7 @@ permissions: contents: write env: + DO_NOT_TRACK: "1" DOCKER_BUILDKIT: 1 TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: "buildwithfern" diff --git a/.github/workflows/update-test.yml b/.github/workflows/update-test.yml index c4d0bf3e403c..80191a1439c1 100644 --- a/.github/workflows/update-test.yml +++ b/.github/workflows/update-test.yml @@ -11,6 +11,9 @@ concurrency: permissions: contents: write +env: + DO_NOT_TRACK: "1" + jobs: update-test: if: github.repository == 'fern-api/fern' && github.ref != 'refs/heads/main' diff --git a/.github/workflows/vale.yml b/.github/workflows/vale.yml index cf54337ed010..5c915fc6922e 100644 --- a/.github/workflows/vale.yml +++ b/.github/workflows/vale.yml @@ -4,6 +4,9 @@ on: paths: - "fern/docs/pages/**" +env: + DO_NOT_TRACK: "1" + jobs: vale: name: runner / vale diff --git a/.github/workflows/validate-changelog.yml b/.github/workflows/validate-changelog.yml index 5d6b814a9728..5e21aa0b0d1d 100644 --- a/.github/workflows/validate-changelog.yml +++ b/.github/workflows/validate-changelog.yml @@ -23,6 +23,7 @@ concurrency: cancel-in-progress: true env: + DO_NOT_TRACK: "1" TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: "buildwithfern" TURBO_REMOTE_CACHE_TIMEOUT: 60 diff --git a/.github/workflows/validate-ir-version-consistency.yml b/.github/workflows/validate-ir-version-consistency.yml index eceab5c7507f..ee3331b7e5fe 100644 --- a/.github/workflows/validate-ir-version-consistency.yml +++ b/.github/workflows/validate-ir-version-consistency.yml @@ -22,6 +22,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true +env: + DO_NOT_TRACK: "1" + jobs: validate-ir-versions: name: Validate IR Version Consistency diff --git a/.github/workflows/validate-versions-files.yml b/.github/workflows/validate-versions-files.yml index f9ba56c16503..e2ca2665172a 100644 --- a/.github/workflows/validate-versions-files.yml +++ b/.github/workflows/validate-versions-files.yml @@ -4,6 +4,9 @@ on: pull_request: workflow_dispatch: +env: + DO_NOT_TRACK: "1" + jobs: validate: name: Validate versions.yml files diff --git a/.github/workflows/write-changelogs-to-docs.yml b/.github/workflows/write-changelogs-to-docs.yml index 39114c37792f..921906bb2761 100644 --- a/.github/workflows/write-changelogs-to-docs.yml +++ b/.github/workflows/write-changelogs-to-docs.yml @@ -10,6 +10,9 @@ permissions: pull-requests: write contents: write +env: + DO_NOT_TRACK: "1" + jobs: copy-changelogs: runs-on: ubuntu-latest diff --git a/fern/apis/docs-yml/generators.yml b/fern/apis/docs-yml/generators.yml index 11fa34303a72..06357918c58f 100644 --- a/fern/apis/docs-yml/generators.yml +++ b/fern/apis/docs-yml/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.2 + version: 3.49.0 output: location: local-file-system path: ../../../packages/cli/configuration/src/docs-yml/schemas/sdk diff --git a/fern/apis/fern-definition/generators.yml b/fern/apis/fern-definition/generators.yml index 3e4f9890e8c4..6d6809458b99 100644 --- a/fern/apis/fern-definition/generators.yml +++ b/fern/apis/fern-definition/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.2 + version: 3.49.0 output: location: local-file-system path: ../../../packages/cli/fern-definition/schema/src/schemas diff --git a/fern/apis/generator-cli/generators.yml b/fern/apis/generator-cli/generators.yml index f7f3b7b02553..892bec26a9e4 100644 --- a/fern/apis/generator-cli/generators.yml +++ b/fern/apis/generator-cli/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.2 + version: 3.49.0 output: location: local-file-system path: ../../../packages/generator-cli/src/configuration/sdk diff --git a/fern/apis/generators-yml/generators.yml b/fern/apis/generators-yml/generators.yml index 753ff6324f61..48e808fa43a7 100644 --- a/fern/apis/generators-yml/generators.yml +++ b/fern/apis/generators-yml/generators.yml @@ -6,7 +6,7 @@ groups: - generators-yml generators: - name: fernapi/fern-typescript-sdk - version: 3.46.2 + version: 3.49.0 output: location: local-file-system path: ../../../packages/cli/configuration/src/generators-yml/schemas diff --git a/generators/base/src/AbstractGeneratorCli.ts b/generators/base/src/AbstractGeneratorCli.ts index cccf3c43dc57..3fa29d418bec 100644 --- a/generators/base/src/AbstractGeneratorCli.ts +++ b/generators/base/src/AbstractGeneratorCli.ts @@ -47,11 +47,11 @@ export abstract class AbstractGeneratorCli< generatorConfig: config, generatorNotificationService }); - context.logger.info(`[TIMING] parseIntermediateRepresentation took ${irElapsed}ms`); - context.logger.info(`[TIMING] constructContext took ${Date.now() - contextStartTime}ms`); + context.logger.debug(`[TIMING] parseIntermediateRepresentation took ${irElapsed}ms`); + context.logger.debug(`[TIMING] constructContext took ${Date.now() - contextStartTime}ms`); const metadataStartTime = Date.now(); await this.generateMetadata(context); - context.logger.info(`[TIMING] generateMetadata took ${Date.now() - metadataStartTime}ms`); + context.logger.debug(`[TIMING] generateMetadata took ${Date.now() - metadataStartTime}ms`); const outputStartTime = Date.now(); switch (config.output.mode.type) { case "publish": @@ -66,7 +66,7 @@ export abstract class AbstractGeneratorCli< default: assertNever(config.output.mode); } - context.logger.info(`[TIMING] output (${config.output.mode.type}) took ${Date.now() - outputStartTime}ms`); + context.logger.debug(`[TIMING] output (${config.output.mode.type}) took ${Date.now() - outputStartTime}ms`); await generatorNotificationService.sendUpdate( FernGeneratorExec.GeneratorUpdate.exitStatusUpdate(FernGeneratorExec.ExitStatusUpdate.successful({})) ); diff --git a/generators/csharp/base/src/project/CsharpProject.ts b/generators/csharp/base/src/project/CsharpProject.ts index c924b52455bd..72cc0409953c 100644 --- a/generators/csharp/base/src/project/CsharpProject.ts +++ b/generators/csharp/base/src/project/CsharpProject.ts @@ -215,26 +215,26 @@ export class CsharpProject extends AbstractProject { libraryPath, solutionPath }); - this.context.logger.info(`[TIMING] createProject took ${Date.now() - createProjectStartTime}ms`); + this.context.logger.debug(`[TIMING] createProject took ${Date.now() - createProjectStartTime}ms`); const createTestProjectStartTime = Date.now(); const absolutePathToTestProjectDirectory = await this.createTestProject({ absolutePathToTestDirectory, absolutePathToSolutionDirectory, absolutePathToProjectDirectory }); - this.context.logger.info(`[TIMING] createTestProject took ${Date.now() - createTestProjectStartTime}ms`); + this.context.logger.debug(`[TIMING] createTestProject took ${Date.now() - createTestProjectStartTime}ms`); const writeSourceFilesStartTime = Date.now(); for (const file of this.sourceFiles) { await file.write(absolutePathToProjectDirectory); } - this.context.logger.info(`[TIMING] writeSourceFiles took ${Date.now() - writeSourceFilesStartTime}ms`); + this.context.logger.debug(`[TIMING] writeSourceFiles took ${Date.now() - writeSourceFilesStartTime}ms`); const writeTestFilesStartTime = Date.now(); for (const file of this.testFiles) { await file.write(absolutePathToTestProjectDirectory); } - this.context.logger.info(`[TIMING] writeTestFiles took ${Date.now() - writeTestFilesStartTime}ms`); + this.context.logger.debug(`[TIMING] writeTestFiles took ${Date.now() - writeTestFilesStartTime}ms`); await this.createRawFiles(); @@ -335,13 +335,13 @@ dotnet_diagnostic.IDE0005.severity = error ` ); } - this.context.logger.info(`[TIMING] dotnetFormat took ${Date.now() - dotnetFormatStartTime}ms`); + this.context.logger.debug(`[TIMING] dotnetFormat took ${Date.now() - dotnetFormatStartTime}ms`); // format the code cleanly using csharpier on the full output directory const csharpierStartTime = Date.now(); await this.csharpier(this.absolutePathToOutputDirectory); - this.context.logger.info(`[TIMING] csharpier took ${Date.now() - csharpierStartTime}ms`); - this.context.logger.info(`[TIMING] persist took ${Date.now() - persistStartTime}ms`); + this.context.logger.debug(`[TIMING] csharpier took ${Date.now() - csharpierStartTime}ms`); + this.context.logger.debug(`[TIMING] persist took ${Date.now() - persistStartTime}ms`); } private async createProject({ diff --git a/generators/csharp/model/src/ModelGeneratorCli.ts b/generators/csharp/model/src/ModelGeneratorCli.ts index 215776f4de54..666bc2badf8f 100644 --- a/generators/csharp/model/src/ModelGeneratorCli.ts +++ b/generators/csharp/model/src/ModelGeneratorCli.ts @@ -59,7 +59,7 @@ export class ModelGeneratorCLI extends AbstractCsharpGeneratorCli { private async generate(context: ModelGeneratorContext): Promise { const generateStartTime = Date.now(); const generatedTypes = generateModels({ context }); - context.logger.info(`[TIMING] generateModels took ${Date.now() - generateStartTime}ms`); + context.logger.debug(`[TIMING] generateModels took ${Date.now() - generateStartTime}ms`); for (const file of generatedTypes) { context.project.addSourceFiles(file); } @@ -73,7 +73,7 @@ export class ModelGeneratorCLI extends AbstractCsharpGeneratorCli { } } - context.logger.info(`[TIMING] code generation took ${Date.now() - generateStartTime}ms`); + context.logger.debug(`[TIMING] code generation took ${Date.now() - generateStartTime}ms`); await context.project.persist(); } } diff --git a/generators/csharp/model/versions.yml b/generators/csharp/model/versions.yml index b5d882115177..b483cf972f67 100644 --- a/generators/csharp/model/versions.yml +++ b/generators/csharp/model/versions.yml @@ -1,4 +1,13 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json +- version: 0.0.8 + changelogEntry: + - summary: | + Change [TIMING] log statements from info to debug level so they are only + printed when debug logging is enabled. + type: fix + createdAt: "2026-02-24" + irVersion: 62 + - version: 0.0.7 changelogEntry: - summary: | diff --git a/generators/csharp/sdk/src/SdkGeneratorCli.ts b/generators/csharp/sdk/src/SdkGeneratorCli.ts index 1f4566d6d38a..31682a026779 100644 --- a/generators/csharp/sdk/src/SdkGeneratorCli.ts +++ b/generators/csharp/sdk/src/SdkGeneratorCli.ts @@ -106,13 +106,13 @@ export class SdkGeneratorCLI extends AbstractCsharpGeneratorCli { // generate names for everything up front. const precalculateStartTime = Date.now(); context.precalculate(); - context.logger.info(`[TIMING] precalculate took ${Date.now() - precalculateStartTime}ms`); + context.logger.debug(`[TIMING] precalculate took ${Date.now() - precalculateStartTime}ms`); // before generating anything, generate the models first so that we // can identify collisions or ambiguities in the generated code. const modelsStartTime = Date.now(); const models = generateModels({ context }); - context.logger.info(`[TIMING] generateModels took ${Date.now() - modelsStartTime}ms`); + context.logger.debug(`[TIMING] generateModels took ${Date.now() - modelsStartTime}ms`); await context.snippetGenerator.populateSnippetsCache(); @@ -296,7 +296,7 @@ export class SdkGeneratorCLI extends AbstractCsharpGeneratorCli { context.logger.warn("Failed to generate reference.md, this is OK."); } } - context.logger.info(`[TIMING] code generation took ${Date.now() - generateStartTime}ms`); + context.logger.debug(`[TIMING] code generation took ${Date.now() - generateStartTime}ms`); await context.project.persist(); } diff --git a/generators/csharp/sdk/versions.yml b/generators/csharp/sdk/versions.yml index c30b34404c45..97aa0e838a2d 100644 --- a/generators/csharp/sdk/versions.yml +++ b/generators/csharp/sdk/versions.yml @@ -1,4 +1,13 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json +- version: 2.22.2 + changelogEntry: + - summary: | + Change [TIMING] log statements from info to debug level so they are only + printed when debug logging is enabled. + type: fix + createdAt: "2026-02-24" + irVersion: 62 + - version: 2.22.1 changelogEntry: - summary: | diff --git a/generators/postman/fern/generators.yml b/generators/postman/fern/generators.yml index 247d2368de17..f4aca800d47e 100644 --- a/generators/postman/fern/generators.yml +++ b/generators/postman/fern/generators.yml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json default-group: sdks groups: sdks: diff --git a/generators/typescript/sdk/versions.yml b/generators/typescript/sdk/versions.yml index eab83b27e6cf..1689affebade 100644 --- a/generators/typescript/sdk/versions.yml +++ b/generators/typescript/sdk/versions.yml @@ -1,4 +1,25 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json +- version: 3.49.1 + changelogEntry: + - summary: | + Change [TIMING] log statements from info to debug level so they are only + printed when debug logging is enabled. + type: fix + createdAt: "2026-02-24" + irVersion: 63 + +- version: 3.49.0 + changelogEntry: + - summary: | + Optimize schema serialization layer performance with 7.2x geometric mean speedup. + Replace O(n²) array/object spread patterns in list and record builders with O(n) loops, + lazily memoize property metadata in object schemas, simplify prototype chain check, + use for-in loops to avoid Object.entries() allocations, cache required-key Sets, + and eliminate rest spread in union discriminant extraction. + type: feat + createdAt: "2026-02-23" + irVersion: 63 + - version: 3.48.5 changelogEntry: - summary: | diff --git a/generators/typescript/utils/abstract-generator-cli/src/AbstractGeneratorCli.ts b/generators/typescript/utils/abstract-generator-cli/src/AbstractGeneratorCli.ts index 3a59a16e08d2..32dc884791e5 100644 --- a/generators/typescript/utils/abstract-generator-cli/src/AbstractGeneratorCli.ts +++ b/generators/typescript/utils/abstract-generator-cli/src/AbstractGeneratorCli.ts @@ -126,7 +126,7 @@ export abstract class AbstractGeneratorCli { generatorContext, intermediateRepresentation: ir }); - logger.info(`[TIMING] code generation took ${Date.now() - codeGenStartTime}ms`); + logger.debug(`[TIMING] code generation took ${Date.now() - codeGenStartTime}ms`); if (!generatorContext.didSucceed()) { throw new Error("Failed to generate TypeScript project."); } diff --git a/generators/typescript/utils/commons/src/core-utilities/Zurg.ts b/generators/typescript/utils/commons/src/core-utilities/Zurg.ts index 7a65423ffd4f..531d55a0dde7 100644 --- a/generators/typescript/utils/commons/src/core-utilities/Zurg.ts +++ b/generators/typescript/utils/commons/src/core-utilities/Zurg.ts @@ -136,7 +136,7 @@ export const MANIFEST: CoreUtility.Manifest = { name: "schemas", pathInCoreUtilities: { nameOnDisk: "schemas", exportDeclaration: { namespaceExport: "serialization" } }, getFilesPatterns: () => { - return { patterns: ["src/core/schemas/**", "tests/unit/schemas/**"] }; + return { patterns: ["src/core/schemas/**", "tests/unit/schemas/**"], ignore: ["**/benchmarks/**"] }; } }; export class ZurgImpl extends CoreUtility implements Zurg { diff --git a/generators/typescript/utils/commons/src/typescript-project/PersistedTypescriptProject.ts b/generators/typescript/utils/commons/src/typescript-project/PersistedTypescriptProject.ts index f14139371a16..368726322c22 100644 --- a/generators/typescript/utils/commons/src/typescript-project/PersistedTypescriptProject.ts +++ b/generators/typescript/utils/commons/src/typescript-project/PersistedTypescriptProject.ts @@ -81,7 +81,7 @@ export class PersistedTypescriptProject { logger.debug("Running npm pkg fix to normalize package.json"); const startTime = Date.now(); await npm(["pkg", "fix"]); - logger.info(`[TIMING] fixPackageJson took ${Date.now() - startTime}ms`); + logger.debug(`[TIMING] fixPackageJson took ${Date.now() - startTime}ms`); } catch (e) { logger.warn(`Failed to run npm pkg fix: ${e}`); } @@ -111,7 +111,7 @@ export class PersistedTypescriptProject { PNPM_FROZEN_LOCKFILE: "false" } })); - logger.info(`[TIMING] generateLockfile took ${Date.now() - startTime}ms`); + logger.debug(`[TIMING] generateLockfile took ${Date.now() - startTime}ms`); } public async installDependencies(logger: Logger): Promise { @@ -138,7 +138,7 @@ export class PersistedTypescriptProject { PNPM_FROZEN_LOCKFILE: "false" } })); - logger.info(`[TIMING] installDependencies took ${Date.now() - startTime}ms`); + logger.debug(`[TIMING] installDependencies took ${Date.now() - startTime}ms`); } public async format(logger: Logger): Promise { @@ -153,7 +153,7 @@ export class PersistedTypescriptProject { try { const startTime = Date.now(); await pm(this.formatCommand); - logger.info(`[TIMING] format took ${Date.now() - startTime}ms`); + logger.debug(`[TIMING] format took ${Date.now() - startTime}ms`); } catch (e) { logger.error(`Failed to format the generated project: ${e}`); } @@ -172,7 +172,7 @@ export class PersistedTypescriptProject { try { const startTime = Date.now(); await pm(this.checkFixCommand); - logger.info(`[TIMING] checkFix took ${Date.now() - startTime}ms`); + logger.debug(`[TIMING] checkFix took ${Date.now() - startTime}ms`); } catch (e) { logger.error(`Failed to format the generated project: ${e}`); } @@ -189,7 +189,7 @@ export class PersistedTypescriptProject { }); const startTime = Date.now(); await pm(this.buildCommand); - logger.info(`[TIMING] build took ${Date.now() - startTime}ms`); + logger.debug(`[TIMING] build took ${Date.now() - startTime}ms`); } public async copyProjectTo({ @@ -370,7 +370,7 @@ export class PersistedTypescriptProject { await npm(publishCommand, { secrets: [publishInfo.registryUrl] }); - logger.info(`[TIMING] publish took ${Date.now() - startTime}ms`); + logger.debug(`[TIMING] publish took ${Date.now() - startTime}ms`); } public async deleteGitIgnoredFiles(logger: Logger): Promise { diff --git a/generators/typescript/utils/core-utilities/src/core/schemas/builders/list/list.ts b/generators/typescript/utils/core-utilities/src/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/generators/typescript/utils/core-utilities/src/core/schemas/builders/list/list.ts +++ b/generators/typescript/utils/core-utilities/src/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/generators/typescript/utils/core-utilities/src/core/schemas/builders/object-like/getObjectLikeUtils.ts b/generators/typescript/utils/core-utilities/src/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..59d1ca64e27c 100644 --- a/generators/typescript/utils/core-utilities/src/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/generators/typescript/utils/core-utilities/src/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,13 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/generators/typescript/utils/core-utilities/src/core/schemas/builders/object/object.ts b/generators/typescript/utils/core-utilities/src/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/generators/typescript/utils/core-utilities/src/core/schemas/builders/object/object.ts +++ b/generators/typescript/utils/core-utilities/src/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/generators/typescript/utils/core-utilities/src/core/schemas/builders/record/record.ts b/generators/typescript/utils/core-utilities/src/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/generators/typescript/utils/core-utilities/src/core/schemas/builders/record/record.ts +++ b/generators/typescript/utils/core-utilities/src/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/generators/typescript/utils/core-utilities/src/core/schemas/builders/union/union.ts b/generators/typescript/utils/core-utilities/src/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/generators/typescript/utils/core-utilities/src/core/schemas/builders/union/union.ts +++ b/generators/typescript/utils/core-utilities/src/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/generators/typescript/utils/core-utilities/src/core/schemas/utils/isPlainObject.ts b/generators/typescript/utils/core-utilities/src/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/generators/typescript/utils/core-utilities/src/core/schemas/utils/isPlainObject.ts +++ b/generators/typescript/utils/core-utilities/src/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/generators/typescript/utils/core-utilities/tests/unit/schemas/benchmarks/serialization.bench.ts b/generators/typescript/utils/core-utilities/tests/unit/schemas/benchmarks/serialization.bench.ts new file mode 100644 index 000000000000..8b51a971e46f --- /dev/null +++ b/generators/typescript/utils/core-utilities/tests/unit/schemas/benchmarks/serialization.bench.ts @@ -0,0 +1,378 @@ +import { bench, describe } from "vitest"; +import { + list, + object, + property, + record, + string, + number, + boolean, + lazy, + lazyObject, + undiscriminatedUnion, +} from "../../../../src/core/schemas/builders"; + +// --------------------------------------------------------------------------- +// Helpers – build fixture data at various scales +// --------------------------------------------------------------------------- + +function makeStringList(n: number): string[] { + return Array.from({ length: n }, (_, i) => `item-${i}`); +} + +function makeObjectList(n: number): Array<{ first_name: string; last_name: string; age: number }> { + return Array.from({ length: n }, (_, i) => ({ + first_name: `first-${i}`, + last_name: `last-${i}`, + age: 20 + (i % 60), + })); +} + +function makeRecord(n: number): Record { + const rec: Record = {}; + for (let i = 0; i < n; i++) { + rec[`key-${i}`] = `value-${i}`; + } + return rec; +} + +function makeNumericRecord(n: number): Record { + const rec: Record = {}; + for (let i = 0; i < n; i++) { + rec[String(i)] = `value-${i}`; + } + return rec; +} + +function makeFlatObject(n: number): Record { + const obj: Record = {}; + for (let i = 0; i < n; i++) { + obj[`field_${i}`] = `value-${i}`; + } + return obj; +} + +function makeNestedIRLike(depth: number, breadth: number): Record { + if (depth === 0) { + return { name: "leaf", value: "data", count: 42 }; + } + const children: Array> = []; + for (let i = 0; i < breadth; i++) { + children.push(makeNestedIRLike(depth - 1, breadth)); + } + return { name: `node-d${depth}`, children, metadata: { key: "val" } }; +} + +// --------------------------------------------------------------------------- +// Schemas +// --------------------------------------------------------------------------- + +const stringListSchema = list(string()); + +const objectListItemSchema = object({ + firstName: property("first_name", string()), + lastName: property("last_name", string()), + age: number(), +}); +const objectListSchema = list(objectListItemSchema); + +const stringRecordSchema = record(string(), string()); +const numericKeyRecordSchema = record(number(), string()); + +// A "wide" object schema with many properties (simulates large IR types) +function buildWideObjectSchema(n: number) { + const schemaProps: Record> = {}; + for (let i = 0; i < n; i++) { + schemaProps[`field${i}`] = property(`field_${i}`, string()); + } + return object(schemaProps); +} +const wideObject10 = buildWideObjectSchema(10); +const wideObject50 = buildWideObjectSchema(50); +const wideObject100 = buildWideObjectSchema(100); + +// Nested object schema (simulates IR nesting) +const innerSchema = object({ + name: string(), + value: string(), + count: number(), +}); + +const nestedSchema = object({ + name: string(), + children: list( + object({ + name: string(), + children: list(innerSchema), + metadata: object({ key: string() }), + }), + ), + metadata: object({ key: string() }), +}); + +// Lazy schema (simulates recursive IR types like recursive unions) +interface TreeNode { + value: string; + children: TreeNode[]; +} +const treeNodeSchema = lazyObject(() => + object({ + value: string(), + children: list(lazy(() => treeNodeSchema)), + }), +); + +function makeTree(depth: number, breadth: number): { value: string; children: unknown[] } { + if (depth === 0) { + return { value: "leaf", children: [] }; + } + return { + value: `node-d${depth}`, + children: Array.from({ length: breadth }, () => makeTree(depth - 1, breadth)), + }; +} + +// --------------------------------------------------------------------------- +// Benchmarks – list (parse + json) +// --------------------------------------------------------------------------- + +describe("list - parse", () => { + const list100 = makeStringList(100); + const list1000 = makeStringList(1_000); + const list10000 = makeStringList(10_000); + + bench("string list (100 items)", () => { + stringListSchema.parseOrThrow(list100); + }); + + bench("string list (1,000 items)", () => { + stringListSchema.parseOrThrow(list1000); + }); + + bench("string list (10,000 items)", () => { + stringListSchema.parseOrThrow(list10000); + }); + + const objList100 = makeObjectList(100); + const objList1000 = makeObjectList(1_000); + + bench("object list (100 items)", () => { + objectListSchema.parseOrThrow(objList100); + }); + + bench("object list (1,000 items)", () => { + objectListSchema.parseOrThrow(objList1000); + }); +}); + +describe("list - json", () => { + const parsed100 = Array.from({ length: 100 }, (_, i) => ({ + firstName: `first-${i}`, + lastName: `last-${i}`, + age: 20 + (i % 60), + })); + const parsed1000 = Array.from({ length: 1_000 }, (_, i) => ({ + firstName: `first-${i}`, + lastName: `last-${i}`, + age: 20 + (i % 60), + })); + + bench("object list json (100 items)", () => { + objectListSchema.jsonOrThrow(parsed100); + }); + + bench("object list json (1,000 items)", () => { + objectListSchema.jsonOrThrow(parsed1000); + }); +}); + +// --------------------------------------------------------------------------- +// Benchmarks – record (parse + json) +// --------------------------------------------------------------------------- + +describe("record - parse", () => { + const rec100 = makeRecord(100); + const rec1000 = makeRecord(1_000); + const rec5000 = makeRecord(5_000); + + bench("string record (100 entries)", () => { + stringRecordSchema.parseOrThrow(rec100); + }); + + bench("string record (1,000 entries)", () => { + stringRecordSchema.parseOrThrow(rec1000); + }); + + bench("string record (5,000 entries)", () => { + stringRecordSchema.parseOrThrow(rec5000); + }); + + const numRec100 = makeNumericRecord(100); + const numRec1000 = makeNumericRecord(1_000); + + bench("numeric-key record (100 entries)", () => { + numericKeyRecordSchema.parseOrThrow(numRec100); + }); + + bench("numeric-key record (1,000 entries)", () => { + numericKeyRecordSchema.parseOrThrow(numRec1000); + }); +}); + +describe("record - json", () => { + const rec100 = makeRecord(100); + const rec1000 = makeRecord(1_000); + + bench("string record json (100 entries)", () => { + stringRecordSchema.jsonOrThrow(rec100); + }); + + bench("string record json (1,000 entries)", () => { + stringRecordSchema.jsonOrThrow(rec1000); + }); +}); + +// --------------------------------------------------------------------------- +// Benchmarks – object (parse + json) +// --------------------------------------------------------------------------- + +describe("object - parse", () => { + const flat10 = makeFlatObject(10); + const flat50 = makeFlatObject(50); + const flat100 = makeFlatObject(100); + + bench("wide object (10 properties)", () => { + wideObject10.parseOrThrow(flat10); + }); + + bench("wide object (50 properties)", () => { + wideObject50.parseOrThrow(flat50); + }); + + bench("wide object (100 properties)", () => { + wideObject100.parseOrThrow(flat100); + }); +}); + +describe("object - json", () => { + // For json, keys are parsed (camelCase) → raw (snake_case) + const parsed10: Record = {}; + for (let i = 0; i < 10; i++) parsed10[`field${i}`] = `value-${i}`; + const parsed50: Record = {}; + for (let i = 0; i < 50; i++) parsed50[`field${i}`] = `value-${i}`; + const parsed100: Record = {}; + for (let i = 0; i < 100; i++) parsed100[`field${i}`] = `value-${i}`; + + bench("wide object json (10 properties)", () => { + wideObject10.jsonOrThrow(parsed10); + }); + + bench("wide object json (50 properties)", () => { + wideObject50.jsonOrThrow(parsed50); + }); + + bench("wide object json (100 properties)", () => { + wideObject100.jsonOrThrow(parsed100); + }); +}); + +// --------------------------------------------------------------------------- +// Benchmarks – nested structures (simulates real IR serialization) +// --------------------------------------------------------------------------- + +describe("nested IR-like structures", () => { + const shallow = makeNestedIRLike(2, 5); // 5^2 = 25 leaf nodes + const medium = makeNestedIRLike(3, 5); // 5^3 = 125 leaf nodes + const deep = makeNestedIRLike(4, 3); // 3^4 = 81 leaf nodes + + bench("nested parse (depth=2, breadth=5, ~25 leaves)", () => { + nestedSchema.parseOrThrow(shallow); + }); + + bench("nested parse (depth=3, breadth=5, ~125 leaves)", () => { + nestedSchema.parseOrThrow(medium); + }); + + bench("nested json (depth=2, breadth=5, ~25 leaves)", () => { + nestedSchema.jsonOrThrow(shallow); + }); + + bench("nested json (depth=3, breadth=5, ~125 leaves)", () => { + nestedSchema.jsonOrThrow(medium); + }); +}); + +// --------------------------------------------------------------------------- +// Benchmarks – lazy/recursive schemas +// --------------------------------------------------------------------------- + +describe("lazy recursive schema", () => { + const tree2 = makeTree(2, 3); // 3^2 = 9 nodes + const tree3 = makeTree(3, 3); // 3^3 = 27 nodes + const tree4 = makeTree(4, 3); // 3^4 = 81 nodes + + bench("lazy tree parse (depth=2, breadth=3, ~9 nodes)", () => { + treeNodeSchema.parseOrThrow(tree2); + }); + + bench("lazy tree parse (depth=3, breadth=3, ~27 nodes)", () => { + treeNodeSchema.parseOrThrow(tree3); + }); + + bench("lazy tree parse (depth=4, breadth=3, ~81 nodes)", () => { + treeNodeSchema.parseOrThrow(tree4); + }); + + bench("lazy tree json (depth=2, breadth=3)", () => { + treeNodeSchema.jsonOrThrow(tree2); + }); + + bench("lazy tree json (depth=3, breadth=3)", () => { + treeNodeSchema.jsonOrThrow(tree3); + }); + + bench("lazy tree json (depth=4, breadth=3)", () => { + treeNodeSchema.jsonOrThrow(tree4); + }); +}); + +// --------------------------------------------------------------------------- +// Benchmarks – object schema construction overhead +// --------------------------------------------------------------------------- + +describe("schema construction", () => { + bench("construct wide object schema (10 props)", () => { + buildWideObjectSchema(10); + }); + + bench("construct wide object schema (50 props)", () => { + buildWideObjectSchema(50); + }); + + bench("construct wide object schema (100 props)", () => { + buildWideObjectSchema(100); + }); +}); + +// --------------------------------------------------------------------------- +// Benchmarks – repeated parse calls (tests caching effectiveness) +// --------------------------------------------------------------------------- + +describe("repeated parse (caching effectiveness)", () => { + const data = makeFlatObject(50); + + // Build a fresh schema each iteration to test first-call overhead + bench("first parse on fresh schema (50 props)", () => { + const schema = buildWideObjectSchema(50); + schema.parseOrThrow(data); + }); + + // Re-use schema to test cached path + const cachedSchema = buildWideObjectSchema(50); + // Warm up the cached schema + cachedSchema.parseOrThrow(data); + + bench("subsequent parse on cached schema (50 props)", () => { + cachedSchema.parseOrThrow(data); + }); +}); diff --git a/packages/cli/api-importers/conjure/conjure-sdk/fern/generators.yml b/packages/cli/api-importers/conjure/conjure-sdk/fern/generators.yml index ec214f9d2642..c6f406e5ecd8 100644 --- a/packages/cli/api-importers/conjure/conjure-sdk/fern/generators.yml +++ b/packages/cli/api-importers/conjure/conjure-sdk/fern/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.2 + version: 3.49.0 output: location: local-file-system path: ../src/sdk diff --git a/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/fetcher/Fetcher.ts b/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/fetcher/Fetcher.ts index 45cae32b23c1..5e5058a9144f 100644 --- a/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/fetcher/Fetcher.ts +++ b/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/fetcher/Fetcher.ts @@ -218,7 +218,13 @@ async function getHeaders(args: Fetcher.Args): Promise { newHeaders.set( "Accept", - args.responseType === "json" ? "application/json" : args.responseType === "text" ? "text/plain" : "*/*", + args.responseType === "json" + ? "application/json" + : args.responseType === "text" + ? "text/plain" + : args.responseType === "sse" + ? "text/event-stream" + : "*/*", ); if (args.body !== undefined && args.contentType != null) { newHeaders.set("Content-Type", args.contentType); diff --git a/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/list/list.ts b/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/list/list.ts +++ b/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts b/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/object/object.ts b/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/object/object.ts +++ b/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/record/record.ts b/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/record/record.ts +++ b/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/union/union.ts b/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/union/union.ts +++ b/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/utils/isPlainObject.ts b/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/utils/isPlainObject.ts +++ b/packages/cli/api-importers/conjure/conjure-sdk/src/sdk/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/packages/cli/api-importers/openapi/openapi-ir/fern/generators.yml b/packages/cli/api-importers/openapi/openapi-ir/fern/generators.yml index 358e67b3b312..fa1694b6fb3a 100644 --- a/packages/cli/api-importers/openapi/openapi-ir/fern/generators.yml +++ b/packages/cli/api-importers/openapi/openapi-ir/fern/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.2 + version: 3.49.0 output: location: local-file-system path: ../src/sdk diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/fetcher/Fetcher.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/fetcher/Fetcher.ts index 45cae32b23c1..5e5058a9144f 100644 --- a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/fetcher/Fetcher.ts +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/fetcher/Fetcher.ts @@ -218,7 +218,13 @@ async function getHeaders(args: Fetcher.Args): Promise { newHeaders.set( "Accept", - args.responseType === "json" ? "application/json" : args.responseType === "text" ? "text/plain" : "*/*", + args.responseType === "json" + ? "application/json" + : args.responseType === "text" + ? "text/plain" + : args.responseType === "sse" + ? "text/event-stream" + : "*/*", ); if (args.body !== undefined && args.contentType != null) { newHeaders.set("Content-Type", args.contentType); diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/list/list.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/list/list.ts +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/object/object.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/object/object.ts +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/record/record.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/record/record.ts +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/union/union.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/union/union.ts +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/utils/isPlainObject.ts b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/utils/isPlainObject.ts +++ b/packages/cli/api-importers/openapi/openapi-ir/src/sdk/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/packages/cli/cli/src/cli.ts b/packages/cli/cli/src/cli.ts index eaed800b7f7f..27a7814f6e71 100644 --- a/packages/cli/cli/src/cli.ts +++ b/packages/cli/cli/src/cli.ts @@ -4,9 +4,11 @@ import { fromBinary, toBinary } from "@bufbuild/protobuf"; import { CodeGeneratorRequestSchema, CodeGeneratorResponseSchema } from "@bufbuild/protobuf/wkt"; import { runCliV2 } from "@fern-api/cli-v2"; import { + correctIncorrectDockerOrg, GENERATORS_CONFIGURATION_FILENAME, generatorsYml, getFernDirectory, + INCORRECT_DOCKER_ORG, loadProjectConfig, PROJECT_CONFIG_FILENAME } from "@fern-api/configuration-loader"; @@ -550,12 +552,13 @@ function addAddCommand(cli: Argv, cliContext: CliContext) { description: "Add the generator to the specified group" }), async (argv) => { + const generatorName = warnAndCorrectIncorrectDockerOrg(argv.generator, cliContext); await addGeneratorToWorkspaces({ project: await loadProjectAndRegisterWorkspacesWithContext(cliContext, { commandLineApiWorkspace: argv.api, defaultToAllApiWorkspaces: false }), - generatorName: argv.generator, + generatorName, groupName: argv.group, cliContext }); @@ -714,6 +717,8 @@ function addGenerateCommand(cli: Argv, cliContext: CliContext) if (argv.output != null && argv.docs != null) { return cliContext.failWithoutThrowing("The --output flag is not supported for docs generation."); } + const correctedGeneratorFilter = + argv.generator != null ? warnAndCorrectIncorrectDockerOrg(argv.generator, cliContext) : undefined; if (argv.api != null) { return await generateAPIWorkspaces({ project: await loadProjectAndRegisterWorkspacesWithContext(cliContext, { @@ -723,7 +728,7 @@ function addGenerateCommand(cli: Argv, cliContext: CliContext) cliContext, version: argv.version, groupName: argv.group, - generatorName: argv.generator, + generatorName: correctedGeneratorFilter, shouldLogS3Url: argv.printZipUrl, keepDocker: argv.keepDocker, useLocalDocker: argv.local || argv.runner != null, @@ -776,7 +781,7 @@ function addGenerateCommand(cli: Argv, cliContext: CliContext) cliContext, version: argv.version, groupName: argv.group, - generatorName: argv.generator, + generatorName: correctedGeneratorFilter, shouldLogS3Url: argv.printZipUrl, keepDocker: argv.keepDocker, useLocalDocker: argv.local, @@ -1993,6 +1998,20 @@ function readBytes(stream: ReadStream): Promise { }); } +/** + * Corrects the incorrect "fern-api/" Docker org prefix to "fernapi/" and logs a warning. + * Used for CLI arguments that accept generator names. + */ +function warnAndCorrectIncorrectDockerOrg(generatorName: string, cliContext: CliContext): string { + const corrected = correctIncorrectDockerOrg(generatorName); + if (corrected !== generatorName) { + cliContext.logger.warn( + `"${generatorName}" is not a valid generator name. Using "${corrected}" instead — the Docker org is "fernapi", not "${INCORRECT_DOCKER_ORG}".` + ); + } + return corrected; +} + function writeBytes(stream: WriteStream, data: Uint8Array): Promise { return new Promise((resolve, reject) => { stream.write(data, (err) => { diff --git a/packages/cli/cli/src/cliV2.ts b/packages/cli/cli/src/cliV2.ts index 2d9f74067cad..e562368c6c68 100644 --- a/packages/cli/cli/src/cliV2.ts +++ b/packages/cli/cli/src/cliV2.ts @@ -1,4 +1,8 @@ -import { GENERATORS_CONFIGURATION_FILENAME } from "@fern-api/configuration-loader"; +import { + correctIncorrectDockerOrg, + GENERATORS_CONFIGURATION_FILENAME, + INCORRECT_DOCKER_ORG +} from "@fern-api/configuration-loader"; import { FernRegistry } from "@fern-fern/generators-sdk"; import { writeFile } from "fs/promises"; import { Argv } from "yargs"; @@ -12,6 +16,20 @@ import { getGeneratorMetadata } from "./commands/generator-metadata/getGenerator import { getOrganization } from "./commands/organization/getOrganization.js"; import { upgradeGenerator } from "./commands/upgrade/upgradeGenerator.js"; +/** + * Corrects the incorrect "fern-api/" Docker org prefix to "fernapi/" and logs a warning. + * Used for CLI arguments that accept generator names. + */ +function warnAndCorrectIncorrectDockerOrgV2(generatorName: string, cliContext: CliContext): string { + const corrected = correctIncorrectDockerOrg(generatorName); + if (corrected !== generatorName) { + cliContext.logger.warn( + `"${generatorName}" is not a valid generator name. Using "${corrected}" instead — the Docker org is "fernapi", not "${INCORRECT_DOCKER_ORG}".` + ); + } + return corrected; +} + export function addGetOrganizationCommand(cli: Argv, cliContext: CliContext): void { cli.command( "organization", @@ -177,6 +195,10 @@ export function addGeneratorCommands(cli: Argv, cliContext: Cl } }); + const correctedGenerator = + argv.generator != null + ? warnAndCorrectIncorrectDockerOrgV2(argv.generator, cliContext) + : undefined; const project = await loadProjectAndRegisterWorkspacesWithContext(cliContext, { commandLineApiWorkspace: argv.api, defaultToAllApiWorkspaces: true @@ -188,7 +210,7 @@ export function addGeneratorCommands(cli: Argv, cliContext: Cl const upgrades = await getProjectGeneratorUpgrades({ cliContext, project, - generatorFilter: argv.generator, + generatorFilter: correctedGenerator, groupFilter: argv.group, includeMajor: argv.includeMajor, channel: argv.channel @@ -205,7 +227,7 @@ export function addGeneratorCommands(cli: Argv, cliContext: Cl } else { await upgradeGenerator({ cliContext, - generator: argv.generator, + generator: correctedGenerator, group: argv.group, project: await loadProjectAndRegisterWorkspacesWithContext(cliContext, { commandLineApiWorkspace: argv.api, @@ -259,10 +281,11 @@ export function addGeneratorCommands(cli: Argv, cliContext: Cl description: "Get repository for the generator invocation, if one is specified." }), async (argv) => { + const correctedGetGenerator = warnAndCorrectIncorrectDockerOrgV2(argv.generator, cliContext); await cliContext.instrumentPostHogEvent({ command: "fern generator get", properties: { - generator: argv.generator, + generator: correctedGetGenerator, version: argv.version, api: argv.api, group: argv.group, @@ -271,7 +294,7 @@ export function addGeneratorCommands(cli: Argv, cliContext: Cl }); const generator = await getGeneratorMetadata({ cliContext, - generatorFilter: argv.generator, + generatorFilter: correctedGetGenerator, groupFilter: argv.group, apiFilter: argv.api, project: await loadProjectAndRegisterWorkspacesWithContext(cliContext, { diff --git a/packages/cli/cli/versions.yml b/packages/cli/cli/versions.yml index dcf3a9dd67fe..0f4d806f4ac3 100644 --- a/packages/cli/cli/versions.yml +++ b/packages/cli/cli/versions.yml @@ -1,4 +1,31 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json +- version: 3.85.2 + changelogEntry: + - summary: | + Regenerate internal SDKs with the latest TypeScript SDK generator to pick up improvements and fixes. + type: chore + createdAt: "2026-02-23" + irVersion: 65 +- version: 3.85.1 + changelogEntry: + - summary: | + Fix AUTO versioning failing for new SDK repositories. When generating an SDK + into an empty or newly initialized repository, the version extraction would fail + because all files are new additions with no previous version lines in the diff. + Now returns 0.0.1 as the initial version instead of throwing an error. + type: fix + createdAt: "2026-02-24" + irVersion: 65 +- version: 3.85.0 + changelogEntry: + - summary: | + Auto-correct incorrect `fern-api/` Docker org prefix to `fernapi/` in generator names. + When a generator is specified with `fern-api/` (the GitHub/npm org) instead of `fernapi/` + (the Docker Hub org), the CLI now automatically corrects it and prints a warning. + This applies to CLI commands (`--generator` flag) and `generators.yml` configuration files. + type: feat + createdAt: "2026-02-23" + irVersion: 65 - version: 3.84.0 changelogEntry: - summary: | @@ -93,6 +120,22 @@ extension (in OpenAPI) with algorithm, encoding, header name, and payload format. SDK generators can use this to produce signature verification utilities. type: feat + - summary: | + Add `terminator` field support for SSE streaming throughout the OpenAPI to Fern pipeline. + The field was already present in the Fern definition schema but missing from the OpenAPI + extension parsing, OpenAPI-IR schema, and the OpenAPI-IR to Fern conversion layers. + type: feat + createdAt: "2026-02-19" + irVersion: 65 +- version: 3.81.1 + changelogEntry: + - summary: | + Fix example object keys starting with `$` (e.g. `$ref`) not being unescaped when + generating the IR `jsonExample`. The Fern definition escapes `$`-prefixed keys with + a backslash to avoid collision with example references, but the backslash was not + removed when building the wire-format JSON example, causing generators to emit + invalid code (e.g. `"\$ref"` in Go). + type: fix createdAt: "2026-02-19" irVersion: 65 - version: 3.81.0 diff --git a/packages/cli/configuration-loader/src/generators-yml/__test__/getGeneratorName.test.ts b/packages/cli/configuration-loader/src/generators-yml/__test__/getGeneratorName.test.ts index b0ed96532fd7..6fc245effaf8 100644 --- a/packages/cli/configuration-loader/src/generators-yml/__test__/getGeneratorName.test.ts +++ b/packages/cli/configuration-loader/src/generators-yml/__test__/getGeneratorName.test.ts @@ -3,8 +3,11 @@ import { createMockTaskContext } from "@fern-api/task-context"; import { GeneratorName } from "../GeneratorName.js"; import { addDefaultDockerOrgIfNotPresent, + correctIncorrectDockerOrg, + correctIncorrectDockerOrgWithWarning, DEFAULT_DOCKER_ORG, getGeneratorNameOrThrow, + INCORRECT_DOCKER_ORG, normalizeGeneratorName, removeDefaultDockerOrgIfPresent } from "../getGeneratorName.js"; @@ -25,6 +28,11 @@ describe("addDefaultDockerOrgIfNotPresent", () => { expect(addDefaultDockerOrgIfNotPresent("fernapi/fern-typescript-sdk")).toBe("fernapi/fern-typescript-sdk"); }); + it("corrects deprecated fern-api/ prefix to fernapi/", () => { + expect(addDefaultDockerOrgIfNotPresent("fern-api/fern-typescript-sdk")).toBe("fernapi/fern-typescript-sdk"); + expect(addDefaultDockerOrgIfNotPresent("fern-api/fern-python-sdk")).toBe("fernapi/fern-python-sdk"); + }); + it("preserves custom org prefixes", () => { expect(addDefaultDockerOrgIfNotPresent("myorg/my-generator")).toBe("myorg/my-generator"); expect(addDefaultDockerOrgIfNotPresent("customorg/fern-typescript-sdk")).toBe("customorg/fern-typescript-sdk"); @@ -239,4 +247,77 @@ describe("real-world scenarios", () => { expect(fdrApiName).toBe("fernapi/fern-csharp-sdk"); expect(fdrApiName).toContain("/"); }); + + it("handles deprecated fern-api/ org in generators.yml", () => { + // User accidentally uses fern-api/ (GitHub org) instead of fernapi/ (Docker org) + const yamlName = "fern-api/fern-typescript-sdk"; + const normalized = addDefaultDockerOrgIfNotPresent(yamlName); + expect(normalized).toBe("fernapi/fern-typescript-sdk"); + }); + + it("handles deprecated fern-api/ org in CLI --generator filter", () => { + // User passes fern-api/fern-typescript-sdk on CLI + const cliArg = "fern-api/fern-typescript-sdk"; + const normalized = addDefaultDockerOrgIfNotPresent(cliArg); + const yamlNormalized = addDefaultDockerOrgIfNotPresent("fern-typescript-sdk"); + expect(normalized).toBe(yamlNormalized); + }); +}); + +describe("correctIncorrectDockerOrg", () => { + it("corrects fern-api/ to fernapi/", () => { + expect(correctIncorrectDockerOrg("fern-api/fern-typescript-sdk")).toBe("fernapi/fern-typescript-sdk"); + expect(correctIncorrectDockerOrg("fern-api/fern-python-sdk")).toBe("fernapi/fern-python-sdk"); + expect(correctIncorrectDockerOrg("fern-api/fern-java-sdk")).toBe("fernapi/fern-java-sdk"); + }); + + it("does not modify fernapi/ names", () => { + expect(correctIncorrectDockerOrg("fernapi/fern-typescript-sdk")).toBe("fernapi/fern-typescript-sdk"); + }); + + it("does not modify shorthand names", () => { + expect(correctIncorrectDockerOrg("fern-typescript-sdk")).toBe("fern-typescript-sdk"); + }); + + it("does not modify custom org names", () => { + expect(correctIncorrectDockerOrg("myorg/my-generator")).toBe("myorg/my-generator"); + }); + + it("handles empty string", () => { + expect(correctIncorrectDockerOrg("")).toBe(""); + }); +}); + +describe("correctIncorrectDockerOrgWithWarning", () => { + it("corrects fern-api/ to fernapi/ and logs warning", () => { + const context = createMockTaskContext(); + const result = correctIncorrectDockerOrgWithWarning("fern-api/fern-typescript-sdk", context); + expect(result).toBe("fernapi/fern-typescript-sdk"); + }); + + it("does not log warning for correct names", () => { + const context = createMockTaskContext(); + const result = correctIncorrectDockerOrgWithWarning("fernapi/fern-typescript-sdk", context); + expect(result).toBe("fernapi/fern-typescript-sdk"); + }); + + it("does not log warning for shorthand names", () => { + const context = createMockTaskContext(); + const result = correctIncorrectDockerOrgWithWarning("fern-typescript-sdk", context); + expect(result).toBe("fern-typescript-sdk"); + }); +}); + +describe("INCORRECT_DOCKER_ORG constant", () => { + it("is set to 'fern-api'", () => { + expect(INCORRECT_DOCKER_ORG).toBe("fern-api"); + }); +}); + +describe("normalizeGeneratorName with deprecated org", () => { + it("normalizes fern-api/ prefixed names to valid GeneratorName", () => { + expect(normalizeGeneratorName("fern-api/fern-typescript-sdk")).toBe(GeneratorName.TYPESCRIPT_SDK); + expect(normalizeGeneratorName("fern-api/fern-python-sdk")).toBe(GeneratorName.PYTHON_SDK); + expect(normalizeGeneratorName("fern-api/fern-java-sdk")).toBe(GeneratorName.JAVA_SDK); + }); }); diff --git a/packages/cli/configuration-loader/src/generators-yml/convertGeneratorsConfiguration.ts b/packages/cli/configuration-loader/src/generators-yml/convertGeneratorsConfiguration.ts index c88529532eb8..6588849b9e40 100644 --- a/packages/cli/configuration-loader/src/generators-yml/convertGeneratorsConfiguration.ts +++ b/packages/cli/configuration-loader/src/generators-yml/convertGeneratorsConfiguration.ts @@ -8,7 +8,7 @@ import { GithubPullRequestReviewer, OutputMetadata, PublishingMetadata, PypiMeta import { readFile } from "fs/promises"; import path from "path"; -import { addDefaultDockerOrgIfNotPresent } from "./getGeneratorName.js"; +import { addDefaultDockerOrgIfNotPresent, correctIncorrectDockerOrgWithWarning } from "./getGeneratorName.js"; /** * Union type representing any spec-level settings schema. @@ -82,7 +82,8 @@ export async function convertGeneratorsConfiguration({ group, maybeTopLevelMetadata, maybeTopLevelReviewers: rawGeneratorsConfiguration.reviewers, - readme + readme, + context }) ) ) @@ -546,7 +547,8 @@ async function convertGroup({ group, maybeTopLevelMetadata, maybeTopLevelReviewers, - readme + readme, + context }: { absolutePathToGeneratorsConfiguration: AbsoluteFilePath; groupName: string; @@ -554,6 +556,7 @@ async function convertGroup({ maybeTopLevelMetadata: OutputMetadata | undefined; maybeTopLevelReviewers: generatorsYml.ReviewersSchema | undefined; readme: generatorsYml.ReadmeSchema | undefined; + context: TaskContext; }): Promise { const maybeGroupLevelMetadata = getOutputMetadata(group.metadata); return { @@ -569,7 +572,8 @@ async function convertGroup({ maybeGroupLevelMetadata, maybeTopLevelReviewers, maybeGroupLevelReviewers: group.reviewers, - readme + readme, + context }) ) ) @@ -583,7 +587,8 @@ async function convertGenerator({ maybeTopLevelMetadata, maybeGroupLevelReviewers, maybeTopLevelReviewers, - readme + readme, + context }: { absolutePathToGeneratorsConfiguration: AbsoluteFilePath; generator: generatorsYml.GeneratorInvocationSchema; @@ -592,9 +597,12 @@ async function convertGenerator({ maybeGroupLevelReviewers: generatorsYml.ReviewersSchema | undefined; maybeTopLevelReviewers: generatorsYml.ReviewersSchema | undefined; readme: generatorsYml.ReadmeSchema | undefined; + context: TaskContext; }): Promise { + // Warn and correct incorrect "fern-api/" org prefix in generators.yml + const correctedName = correctIncorrectDockerOrgWithWarning(generator.name, context); // Normalize the generator name by adding the default Docker org prefix if not present - const normalizedName = addDefaultDockerOrgIfNotPresent(generator.name); + const normalizedName = addDefaultDockerOrgIfNotPresent(correctedName); return { raw: generator, name: normalizedName, diff --git a/packages/cli/configuration-loader/src/generators-yml/getGeneratorName.ts b/packages/cli/configuration-loader/src/generators-yml/getGeneratorName.ts index 3d2e1372b425..9664105de38a 100644 --- a/packages/cli/configuration-loader/src/generators-yml/getGeneratorName.ts +++ b/packages/cli/configuration-loader/src/generators-yml/getGeneratorName.ts @@ -3,6 +3,7 @@ import { TaskContext } from "@fern-api/task-context"; import { GeneratorName } from "./GeneratorName.js"; export const DEFAULT_DOCKER_ORG = "fernapi"; +export const INCORRECT_DOCKER_ORG = "fern-api"; /** * Adds the default Docker org prefix (fernapi/) to a generator name if no org is specified. @@ -15,12 +16,48 @@ export const DEFAULT_DOCKER_ORG = "fernapi"; * - "myorg/my-generator" -> "myorg/my-generator" (unchanged) */ export function addDefaultDockerOrgIfNotPresent(generatorName: string): string { + // Correct incorrect "fern-api/" org to "fernapi/" silently + generatorName = correctIncorrectDockerOrg(generatorName); if (generatorName.includes("/")) { return generatorName; } return `${DEFAULT_DOCKER_ORG}/${generatorName}`; } +/** + * Corrects the incorrect Docker org prefix "fern-api/" to the correct "fernapi/". + * This handles a common user mistake where "fern-api" (the GitHub/npm org) is + * confused with "fernapi" (the Docker Hub org for Fern generators). + * + * Examples: + * - "fern-api/fern-typescript-sdk" -> "fernapi/fern-typescript-sdk" + * - "fernapi/fern-typescript-sdk" -> "fernapi/fern-typescript-sdk" (unchanged) + * - "myorg/my-generator" -> "myorg/my-generator" (unchanged) + */ +export function correctIncorrectDockerOrg(generatorName: string): string { + const incorrectPrefix = `${INCORRECT_DOCKER_ORG}/`; + if (generatorName.startsWith(incorrectPrefix)) { + return `${DEFAULT_DOCKER_ORG}/${generatorName.slice(incorrectPrefix.length)}`; + } + return generatorName; +} + +/** + * Corrects the incorrect Docker org prefix "fern-api/" to "fernapi/" and logs a warning. + * Use this variant when you have access to a TaskContext for user-visible warnings. + */ +export function correctIncorrectDockerOrgWithWarning(generatorName: string, context: TaskContext): string { + const incorrectPrefix = `${INCORRECT_DOCKER_ORG}/`; + if (generatorName.startsWith(incorrectPrefix)) { + const corrected = `${DEFAULT_DOCKER_ORG}/${generatorName.slice(incorrectPrefix.length)}`; + context.logger.warn( + `"${generatorName}" is not a valid generator name. Using "${corrected}" instead — the Docker org is "fernapi", not "fern-api".` + ); + return corrected; + } + return generatorName; +} + /** * Removes the default Docker org prefix (fernapi/) from a generator name if present. * If the name has a different org prefix, it is returned as-is. diff --git a/packages/cli/configuration-loader/src/generators-yml/index.ts b/packages/cli/configuration-loader/src/generators-yml/index.ts index daf10744a471..d305c87d14a4 100644 --- a/packages/cli/configuration-loader/src/generators-yml/index.ts +++ b/packages/cli/configuration-loader/src/generators-yml/index.ts @@ -8,7 +8,11 @@ export { GeneratorName } from "./GeneratorName.js"; export { GENERATOR_INVOCATIONS } from "./generatorInvocations.js"; export { addDefaultDockerOrgIfNotPresent, + correctIncorrectDockerOrg, + correctIncorrectDockerOrgWithWarning, + DEFAULT_DOCKER_ORG, getGeneratorNameOrThrow, + INCORRECT_DOCKER_ORG, normalizeGeneratorName, removeDefaultDockerOrgIfPresent } from "./getGeneratorName.js"; diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/core/fetcher/Fetcher.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/core/fetcher/Fetcher.ts index 45cae32b23c1..5e5058a9144f 100644 --- a/packages/cli/configuration/src/docs-yml/schemas/sdk/core/fetcher/Fetcher.ts +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/core/fetcher/Fetcher.ts @@ -218,7 +218,13 @@ async function getHeaders(args: Fetcher.Args): Promise { newHeaders.set( "Accept", - args.responseType === "json" ? "application/json" : args.responseType === "text" ? "text/plain" : "*/*", + args.responseType === "json" + ? "application/json" + : args.responseType === "text" + ? "text/plain" + : args.responseType === "sse" + ? "text/event-stream" + : "*/*", ); if (args.body !== undefined && args.contentType != null) { newHeaders.set("Content-Type", args.contentType); diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/list/list.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/list/list.ts +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/object/object.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/object/object.ts +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/record/record.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/record/record.ts +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/union/union.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/union/union.ts +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/utils/isPlainObject.ts b/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/utils/isPlainObject.ts +++ b/packages/cli/configuration/src/docs-yml/schemas/sdk/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/packages/cli/configuration/src/generators-yml/schemas/core/fetcher/Fetcher.ts b/packages/cli/configuration/src/generators-yml/schemas/core/fetcher/Fetcher.ts index 45cae32b23c1..5e5058a9144f 100644 --- a/packages/cli/configuration/src/generators-yml/schemas/core/fetcher/Fetcher.ts +++ b/packages/cli/configuration/src/generators-yml/schemas/core/fetcher/Fetcher.ts @@ -218,7 +218,13 @@ async function getHeaders(args: Fetcher.Args): Promise { newHeaders.set( "Accept", - args.responseType === "json" ? "application/json" : args.responseType === "text" ? "text/plain" : "*/*", + args.responseType === "json" + ? "application/json" + : args.responseType === "text" + ? "text/plain" + : args.responseType === "sse" + ? "text/event-stream" + : "*/*", ); if (args.body !== undefined && args.contentType != null) { newHeaders.set("Content-Type", args.contentType); diff --git a/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/list/list.ts b/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/list/list.ts +++ b/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/object-like/getObjectLikeUtils.ts b/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/object/object.ts b/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/object/object.ts +++ b/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/record/record.ts b/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/record/record.ts +++ b/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/union/union.ts b/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/union/union.ts +++ b/packages/cli/configuration/src/generators-yml/schemas/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/packages/cli/configuration/src/generators-yml/schemas/core/schemas/utils/isPlainObject.ts b/packages/cli/configuration/src/generators-yml/schemas/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/packages/cli/configuration/src/generators-yml/schemas/core/schemas/utils/isPlainObject.ts +++ b/packages/cli/configuration/src/generators-yml/schemas/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/packages/cli/configuration/src/generators-yml/schemas/serialization/resources/generators/types/GraphQlSpecSchema.ts b/packages/cli/configuration/src/generators-yml/schemas/serialization/resources/generators/types/GraphQlSpecSchema.ts index fb2b685392a0..0a00013a1368 100644 --- a/packages/cli/configuration/src/generators-yml/schemas/serialization/resources/generators/types/GraphQlSpecSchema.ts +++ b/packages/cli/configuration/src/generators-yml/schemas/serialization/resources/generators/types/GraphQlSpecSchema.ts @@ -1,8 +1,8 @@ // This file was auto-generated by Fern from our API Definition. -import * as serializers from "../../../index.js"; -import * as GeneratorsYml from "../../../../api/index.js"; +import type * as GeneratorsYml from "../../../../api/index.js"; import * as core from "../../../../core/index.js"; +import type * as serializers from "../../../index.js"; export const GraphQlSpecSchema: core.serialization.ObjectSchema< serializers.GraphQlSpecSchema.Raw, diff --git a/packages/cli/fern-definition/schema/src/schemas/api/resources/examples/types/ExampleBodyResponseSchema.ts b/packages/cli/fern-definition/schema/src/schemas/api/resources/examples/types/ExampleBodyResponseSchema.ts index 732fecc94f91..007cafe5e252 100644 --- a/packages/cli/fern-definition/schema/src/schemas/api/resources/examples/types/ExampleBodyResponseSchema.ts +++ b/packages/cli/fern-definition/schema/src/schemas/api/resources/examples/types/ExampleBodyResponseSchema.ts @@ -4,5 +4,5 @@ import type * as FernDefinition from "../../../index.js"; export interface ExampleBodyResponseSchema { error?: string; - body?: FernDefinition.ExampleTypeReferenceSchema | undefined; + body?: FernDefinition.ExampleTypeReferenceSchema; } diff --git a/packages/cli/fern-definition/schema/src/schemas/api/resources/examples/types/ExampleEndpointCallSchema.ts b/packages/cli/fern-definition/schema/src/schemas/api/resources/examples/types/ExampleEndpointCallSchema.ts index e68689930b1c..3645f097e416 100644 --- a/packages/cli/fern-definition/schema/src/schemas/api/resources/examples/types/ExampleEndpointCallSchema.ts +++ b/packages/cli/fern-definition/schema/src/schemas/api/resources/examples/types/ExampleEndpointCallSchema.ts @@ -7,7 +7,7 @@ export interface ExampleEndpointCallSchema extends FernDefinition.WithName, Fern "path-parameters"?: Record; "query-parameters"?: Record; headers?: Record; - request?: FernDefinition.ExampleTypeReferenceSchema | undefined; + request?: FernDefinition.ExampleTypeReferenceSchema; response?: FernDefinition.ExampleResponseSchema; "code-samples"?: FernDefinition.ExampleCodeSampleSchema[]; } diff --git a/packages/cli/fern-definition/schema/src/schemas/api/resources/examples/types/ExampleSseEventSchema.ts b/packages/cli/fern-definition/schema/src/schemas/api/resources/examples/types/ExampleSseEventSchema.ts index eacf3c0d6135..26162ce32372 100644 --- a/packages/cli/fern-definition/schema/src/schemas/api/resources/examples/types/ExampleSseEventSchema.ts +++ b/packages/cli/fern-definition/schema/src/schemas/api/resources/examples/types/ExampleSseEventSchema.ts @@ -4,5 +4,5 @@ import type * as FernDefinition from "../../../index.js"; export interface ExampleSseEventSchema { event: string; - data?: FernDefinition.ExampleTypeReferenceSchema | undefined; + data?: FernDefinition.ExampleTypeReferenceSchema; } diff --git a/packages/cli/fern-definition/schema/src/schemas/core/fetcher/Fetcher.ts b/packages/cli/fern-definition/schema/src/schemas/core/fetcher/Fetcher.ts index 45cae32b23c1..5e5058a9144f 100644 --- a/packages/cli/fern-definition/schema/src/schemas/core/fetcher/Fetcher.ts +++ b/packages/cli/fern-definition/schema/src/schemas/core/fetcher/Fetcher.ts @@ -218,7 +218,13 @@ async function getHeaders(args: Fetcher.Args): Promise { newHeaders.set( "Accept", - args.responseType === "json" ? "application/json" : args.responseType === "text" ? "text/plain" : "*/*", + args.responseType === "json" + ? "application/json" + : args.responseType === "text" + ? "text/plain" + : args.responseType === "sse" + ? "text/event-stream" + : "*/*", ); if (args.body !== undefined && args.contentType != null) { newHeaders.set("Content-Type", args.contentType); diff --git a/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/list/list.ts b/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/list/list.ts +++ b/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/object-like/getObjectLikeUtils.ts b/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/object/object.ts b/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/object/object.ts +++ b/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/record/record.ts b/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/record/record.ts +++ b/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/union/union.ts b/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/union/union.ts +++ b/packages/cli/fern-definition/schema/src/schemas/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/packages/cli/fern-definition/schema/src/schemas/core/schemas/utils/isPlainObject.ts b/packages/cli/fern-definition/schema/src/schemas/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/packages/cli/fern-definition/schema/src/schemas/core/schemas/utils/isPlainObject.ts +++ b/packages/cli/fern-definition/schema/src/schemas/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/packages/cli/generation/ir-migrations/package.json b/packages/cli/generation/ir-migrations/package.json index 8227e4b2f729..1c54a4a5a6f3 100644 --- a/packages/cli/generation/ir-migrations/package.json +++ b/packages/cli/generation/ir-migrations/package.json @@ -96,7 +96,7 @@ "@fern-fern/ir-v56-sdk": "catalog:", "@fern-fern/ir-v57-sdk": "catalog:", "@fern-fern/ir-v58-sdk": "catalog:", - "@fern-fern/ir-v59-sdk": "1.0.0", + "@fern-fern/ir-v59-sdk": "catalog:", "@fern-fern/ir-v6-model": "catalog:", "@fern-fern/ir-v60-sdk": "catalog:", "@fern-fern/ir-v61-sdk": "catalog:", diff --git a/packages/cli/generation/local-generation/local-workspace-runner/src/AutoVersioningService.ts b/packages/cli/generation/local-generation/local-workspace-runner/src/AutoVersioningService.ts index 0614d22b9fb4..ebe1fde03950 100644 --- a/packages/cli/generation/local-generation/local-workspace-runner/src/AutoVersioningService.ts +++ b/packages/cli/generation/local-generation/local-workspace-runner/src/AutoVersioningService.ts @@ -47,10 +47,10 @@ export class AutoVersioningService { * * @param diffContent The git diff content * @param mappedMagicVersion The magic version after language transformations (e.g., "v505.503.4455" for Go) - * @return The previous version if found - * @throws AutoVersioningException if no previous version can be extracted from the diff + * @return The previous version if found, or undefined if all magic version occurrences are in new files + * @throws AutoVersioningException if the magic version is not found at all in the diff */ - public extractPreviousVersion(diffContent: string, mappedMagicVersion: string): string { + public extractPreviousVersion(diffContent: string, mappedMagicVersion: string): string | undefined { const lines = diffContent.split("\n"); let currentFile = "unknown"; @@ -89,10 +89,11 @@ export class AutoVersioningService { } if (magicVersionOccurrences > 0) { - throw new AutoVersioningException( + this.logger.info( `Found placeholder version in the diff but no matching previous version lines were found in any hunk. ` + - `This may indicate new files or a format change. occurrences=${magicVersionOccurrences}, pairsFound=0` + `This indicates a new SDK repository with no previous version. occurrences=${magicVersionOccurrences}` ); + return undefined; } throw new AutoVersioningException( diff --git a/packages/cli/generation/local-generation/local-workspace-runner/src/LocalTaskHandler.ts b/packages/cli/generation/local-generation/local-workspace-runner/src/LocalTaskHandler.ts index c465fea3e5f5..eacced847a82 100644 --- a/packages/cli/generation/local-generation/local-workspace-runner/src/LocalTaskHandler.ts +++ b/packages/cli/generation/local-generation/local-workspace-runner/src/LocalTaskHandler.ts @@ -139,6 +139,22 @@ export class LocalTaskHandler { this.context.logger.debug(`Generated diff size: ${diffContent.length} bytes`); this.context.logger.debug(`Cleaned diff size: ${cleanedDiff.length} bytes`); + + // Handle new SDK repository with no previous version + if (previousVersion == null) { + this.context.logger.info( + "No previous version found (new SDK repository). Using 0.0.1 as initial version." + ); + const initialVersion = this.version?.startsWith("v") ? "v0.0.1" : "0.0.1"; + const commitMessage = this.isWhitelabel + ? "Initial SDK generation" + : "Initial SDK generation\n\n🌿 Generated with Fern"; + return { + version: initialVersion, + commitMessage + }; + } + this.context.logger.debug(`Previous version detected: ${previousVersion}`); // Call AI to analyze the diff diff --git a/packages/cli/generation/local-generation/local-workspace-runner/src/__test__/AutoVersioningService.test.ts b/packages/cli/generation/local-generation/local-workspace-runner/src/__test__/AutoVersioningService.test.ts index 0fb8090c6ef2..dd5ffcc1508b 100644 --- a/packages/cli/generation/local-generation/local-workspace-runner/src/__test__/AutoVersioningService.test.ts +++ b/packages/cli/generation/local-generation/local-workspace-runner/src/__test__/AutoVersioningService.test.ts @@ -99,7 +99,7 @@ describe("AutoVersioningService", () => { expect(previousVersion).toBe("3.0.0-beta.2"); }); - it("testExtractPreviousVersion_noPreviousVersion", () => { + it("testExtractPreviousVersion_noPreviousVersion_returnsUndefined", () => { const diff = "diff --git a/new-file.txt b/new-file.txt\n" + "new file mode 100644\n" + @@ -109,9 +109,45 @@ describe("AutoVersioningService", () => { "@@ -0,0 +1 @@\n" + "+version = 505.503.4455\n"; - expect(() => { - new AutoVersioningService({ logger: mockLogger }).extractPreviousVersion(diff, "505.503.4455"); - }).toThrow(AutoVersioningException); + const result = new AutoVersioningService({ logger: mockLogger }).extractPreviousVersion(diff, "505.503.4455"); + expect(result).toBeUndefined(); + }); + + it("testExtractPreviousVersion_newRepoWithMultipleNewFiles_returnsUndefined", () => { + // Simulates a new SDK repo where all files are new (like immix-ts-sdk initial generation) + const diff = + "diff --git a/package.json b/package.json\n" + + "new file mode 100644\n" + + "index 0000000..abc123\n" + + "--- /dev/null\n" + + "+++ b/package.json\n" + + "@@ -0,0 +1,6 @@\n" + + "+{\n" + + '+ "name": "test-sdk",\n' + + '+ "version": "505.503.4455",\n' + + '+ "private": false\n' + + "+}\n" + + "diff --git a/src/Client.ts b/src/Client.ts\n" + + "new file mode 100644\n" + + "index 0000000..def456\n" + + "--- /dev/null\n" + + "+++ b/src/Client.ts\n" + + "@@ -0,0 +1,5 @@\n" + + "+export class Client {\n" + + '+ private readonly version = "505.503.4455";\n' + + "+ constructor() {}\n" + + "+}\n" + + "diff --git a/src/core/index.ts b/src/core/index.ts\n" + + "new file mode 100644\n" + + "index 0000000..789abc\n" + + "--- /dev/null\n" + + "+++ b/src/core/index.ts\n" + + "@@ -0,0 +1,3 @@\n" + + '+export const SDK_VERSION = "505.503.4455";\n' + + '+export const SDK_NAME = "test-sdk";\n'; + + const result = new AutoVersioningService({ logger: mockLogger }).extractPreviousVersion(diff, "505.503.4455"); + expect(result).toBeUndefined(); }); it("testCleanDiffForAI_removesMagicVersionLines", () => { @@ -222,7 +258,7 @@ describe("AutoVersioningService", () => { expect(cleaned).not.toContain("version.go"); }); - it("testExtractPreviousVersion_invalidVersionFormat", () => { + it("testExtractPreviousVersion_invalidVersionFormat_returnsUndefined", () => { const diff = "diff --git a/config.txt b/config.txt\n" + "index abc123..def456 100644\n" + @@ -232,9 +268,10 @@ describe("AutoVersioningService", () => { "-some random text\n" + "+magic version is 505.503.4455\n"; - expect(() => { - new AutoVersioningService({ logger: mockLogger }).extractPreviousVersion(diff, "505.503.4455"); - }).toThrow(AutoVersioningException); + // The minus line doesn't form a version pair, so no previous version can be extracted. + // This is treated like a new file scenario - returns undefined instead of throwing. + const result = new AutoVersioningService({ logger: mockLogger }).extractPreviousVersion(diff, "505.503.4455"); + expect(result).toBeUndefined(); }); it("testExtractPreviousVersion_withNonVersionLinesInBetween", () => { @@ -285,7 +322,7 @@ describe("AutoVersioningService", () => { expect(previousVersion).toBe("1.5.0"); }); - it("testExtractPreviousVersion_noMatchingMinusLines_throwsException", () => { + it("testExtractPreviousVersion_noMatchingMinusLines_returnsUndefined", () => { const diff = "diff --git a/README.md b/README.md\n" + "index abc123..def456 100644\n" + @@ -302,14 +339,8 @@ describe("AutoVersioningService", () => { " # Changelog\n" + "+## 505.503.4455\n"; - try { - new AutoVersioningService({ logger: mockLogger }).extractPreviousVersion(diff, "505.503.4455"); - expect.fail("Should have thrown AutoVersioningException"); - } catch (error) { - expect(error).toBeInstanceOf(AutoVersioningException); - expect((error as Error).message).toContain("no matching previous version lines were found"); - expect((error as Error).message).toContain("occurrences=2"); - } + const result = new AutoVersioningService({ logger: mockLogger }).extractPreviousVersion(diff, "505.503.4455"); + expect(result).toBeUndefined(); }); it("testCleanDiffForAI_removesFileWithOnlyVersionChanges", () => { @@ -722,6 +753,374 @@ describe("AutoVersioningService", () => { } }); + // --- Tests for extractPreviousVersion: exception path --- + + it("testExtractPreviousVersion_noMagicVersionInDiff_throws", () => { + // When the magic version is not found at all in the diff, it should throw + const diff = + "diff --git a/README.md b/README.md\n" + + "index abc123..def456 100644\n" + + "--- a/README.md\n" + + "+++ b/README.md\n" + + "@@ -1,3 +1,4 @@\n" + + " # My Project\n" + + "+Some new content\n" + + " ## Installation\n"; + + expect(() => + new AutoVersioningService({ logger: mockLogger }).extractPreviousVersion(diff, "505.503.4455") + ).toThrow(AutoVersioningException); + }); + + it("testExtractPreviousVersion_emptyDiff_throws", () => { + expect(() => + new AutoVersioningService({ logger: mockLogger }).extractPreviousVersion("", "505.503.4455") + ).toThrow(AutoVersioningException); + }); + + it("testExtractPreviousVersion_magicVersionOnlyInContextLines_throws", () => { + // Magic version appears in context lines (no + or - prefix), should not count + const diff = + "diff --git a/config.txt b/config.txt\n" + + "index abc123..def456 100644\n" + + "--- a/config.txt\n" + + "+++ b/config.txt\n" + + "@@ -1,3 +1,4 @@\n" + + " version = 505.503.4455\n" + + "+new line added\n" + + " other content\n"; + + expect(() => + new AutoVersioningService({ logger: mockLogger }).extractPreviousVersion(diff, "505.503.4455") + ).toThrow(AutoVersioningException); + }); + + // --- Tests for extractPreviousVersion: new repo with v prefix --- + + it("testExtractPreviousVersion_newRepoWithVPrefixMagicVersion_returnsUndefined", () => { + // Go SDK new repo: all files are new, magic version has v prefix + const diff = + "diff --git a/version.go b/version.go\n" + + "new file mode 100644\n" + + "index 0000000..abc123\n" + + "--- /dev/null\n" + + "+++ b/version.go\n" + + "@@ -0,0 +1,3 @@\n" + + "+package sdk\n" + + '+const Version = "v505.503.4455"\n' + + "+\n" + + "diff --git a/client.go b/client.go\n" + + "new file mode 100644\n" + + "index 0000000..def456\n" + + "--- /dev/null\n" + + "+++ b/client.go\n" + + "@@ -0,0 +1,5 @@\n" + + "+package sdk\n" + + "+\n" + + "+type Client struct {\n" + + "+ version string // v505.503.4455\n" + + "+}\n"; + + const result = new AutoVersioningService({ logger: mockLogger }).extractPreviousVersion(diff, "v505.503.4455"); + expect(result).toBeUndefined(); + }); + + it("testExtractPreviousVersion_singleNewFileWithVPrefix_returnsUndefined", () => { + const diff = + "diff --git a/version.go b/version.go\n" + + "new file mode 100644\n" + + "index 0000000..abc123\n" + + "--- /dev/null\n" + + "+++ b/version.go\n" + + "@@ -0,0 +1 @@\n" + + '+const Version = "v505.503.4455"\n'; + + const result = new AutoVersioningService({ logger: mockLogger }).extractPreviousVersion(diff, "v505.503.4455"); + expect(result).toBeUndefined(); + }); + + // --- Tests for extractPreviousVersion: mixed scenario --- + + it("testExtractPreviousVersion_mixedNewAndExistingFiles_returnsVersionFromExisting", () => { + // Some files are new (no matching minus lines), but one existing file has a version pair + const diff = + "diff --git a/src/newFile.ts b/src/newFile.ts\n" + + "new file mode 100644\n" + + "index 0000000..abc123\n" + + "--- /dev/null\n" + + "+++ b/src/newFile.ts\n" + + "@@ -0,0 +1,3 @@\n" + + '+export const SDK_VERSION = "505.503.4455";\n' + + "+export const name = 'test';\n" + + "diff --git a/package.json b/package.json\n" + + "index abc123..def456 100644\n" + + "--- a/package.json\n" + + "+++ b/package.json\n" + + "@@ -1,5 +1,5 @@\n" + + " {\n" + + '- "version": "2.3.4",\n' + + '+ "version": "505.503.4455",\n' + + ' "name": "test"\n' + + " }\n"; + + const previousVersion = new AutoVersioningService({ logger: mockLogger }).extractPreviousVersion( + diff, + "505.503.4455" + ); + expect(previousVersion).toBe("2.3.4"); + }); + + it("testExtractPreviousVersion_mixedNewAndExistingFiles_newFileFirst_returnsVersionFromExisting", () => { + // New file appears first in the diff, followed by existing file with version pair. + // The new file's magic version occurrence should be skipped, then find match in existing file. + const diff = + "diff --git a/src/version.ts b/src/version.ts\n" + + "new file mode 100644\n" + + "index 0000000..abc123\n" + + "--- /dev/null\n" + + "+++ b/src/version.ts\n" + + "@@ -0,0 +1,1 @@\n" + + '+export const VERSION = "505.503.4455";\n' + + "diff --git a/src/Client.ts b/src/Client.ts\n" + + "new file mode 100644\n" + + "index 0000000..def456\n" + + "--- /dev/null\n" + + "+++ b/src/Client.ts\n" + + "@@ -0,0 +1,2 @@\n" + + '+import { VERSION } from "./version";\n' + + '+const v = "505.503.4455";\n' + + "diff --git a/package.json b/package.json\n" + + "index 111111..222222 100644\n" + + "--- a/package.json\n" + + "+++ b/package.json\n" + + "@@ -2,3 +2,3 @@\n" + + ' "name": "my-sdk",\n' + + '- "version": "1.0.0",\n' + + '+ "version": "505.503.4455",\n' + + ' "private": false\n'; + + const previousVersion = new AutoVersioningService({ logger: mockLogger }).extractPreviousVersion( + diff, + "505.503.4455" + ); + expect(previousVersion).toBe("1.0.0"); + }); + + // --- Tests for cleanDiffForAI: new file scenarios --- + + it("testCleanDiffForAI_allNewFiles_removesAllVersionLines", () => { + // Simulates a new SDK repo where all files are new (all + lines) + const diff = + "diff --git a/package.json b/package.json\n" + + "new file mode 100644\n" + + "index 0000000..abc123\n" + + "--- /dev/null\n" + + "+++ b/package.json\n" + + "@@ -0,0 +1,5 @@\n" + + "+{\n" + + '+ "name": "test-sdk",\n' + + '+ "version": "505.503.4455",\n' + + '+ "private": false\n' + + "+}\n" + + "diff --git a/src/Client.ts b/src/Client.ts\n" + + "new file mode 100644\n" + + "index 0000000..def456\n" + + "--- /dev/null\n" + + "+++ b/src/Client.ts\n" + + "@@ -0,0 +1,3 @@\n" + + "+export class Client {\n" + + "+ constructor() {}\n" + + "+}\n"; + + const cleaned = new AutoVersioningService({ logger: mockLogger }).cleanDiffForAI(diff, "505.503.4455"); + + // The version line should be removed + expect(cleaned).not.toContain("505.503.4455"); + // Other new file content should be preserved + expect(cleaned).toContain('+ "name": "test-sdk"'); + expect(cleaned).toContain("+export class Client"); + }); + + it("testCleanDiffForAI_newFileWithVPrefixMagicVersion", () => { + // Go SDK new file scenario with v-prefixed magic version + const diff = + "diff --git a/version.go b/version.go\n" + + "new file mode 100644\n" + + "index 0000000..abc123\n" + + "--- /dev/null\n" + + "+++ b/version.go\n" + + "@@ -0,0 +1,3 @@\n" + + "+package sdk\n" + + '+const Version = "v505.503.4455"\n' + + "+\n"; + + const cleaned = new AutoVersioningService({ logger: mockLogger }).cleanDiffForAI(diff, "v505.503.4455"); + + expect(cleaned).not.toContain("v505.503.4455"); + expect(cleaned).toContain("+package sdk"); + }); + + // --- Tests for replaceMagicVersion: initial version scenarios --- + + it("testReplaceMagicVersion_withVPrefixInitialVersion", async () => { + // Simulates Go SDK initial generation: replacing v505.503.4455 with v0.0.1 + const tempDir = await fs.mkdtemp(path.join(require("os").tmpdir(), "test-")); + try { + const versionFile = path.join(tempDir, "version.go"); + await fs.writeFile(versionFile, 'package sdk\n\nconst Version = "v505.503.4455"\n'); + + const clientFile = path.join(tempDir, "client.go"); + await fs.writeFile(clientFile, 'package sdk\n\nvar sdkVersion = "v505.503.4455"\n'); + + await new AutoVersioningService({ logger: mockLogger }).replaceMagicVersion( + tempDir, + "v505.503.4455", + "v0.0.1" + ); + + const versionContent = await fs.readFile(versionFile, "utf-8"); + expect(versionContent).not.toContain("v505.503.4455"); + expect(versionContent).toContain("v0.0.1"); + expect(versionContent).toBe('package sdk\n\nconst Version = "v0.0.1"\n'); + + const clientContent = await fs.readFile(clientFile, "utf-8"); + expect(clientContent).not.toContain("v505.503.4455"); + expect(clientContent).toContain("v0.0.1"); + } finally { + await fs.rm(tempDir, { recursive: true, force: true }); + } + }); + + it("testReplaceMagicVersion_withNonVPrefixInitialVersion", async () => { + // Simulates TS/Python SDK initial generation: replacing 505.503.4455 with 0.0.1 + const tempDir = await fs.mkdtemp(path.join(require("os").tmpdir(), "test-")); + try { + const packageJson = path.join(tempDir, "package.json"); + await fs.writeFile(packageJson, '{\n "name": "test-sdk",\n "version": "505.503.4455"\n}'); + + const clientFile = path.join(tempDir, "src"); + await fs.mkdir(clientFile, { recursive: true }); + const versionTs = path.join(clientFile, "version.ts"); + await fs.writeFile(versionTs, 'export const SDK_VERSION = "505.503.4455";\n'); + + await new AutoVersioningService({ logger: mockLogger }).replaceMagicVersion( + tempDir, + "505.503.4455", + "0.0.1" + ); + + const packageContent = await fs.readFile(packageJson, "utf-8"); + expect(packageContent).not.toContain("505.503.4455"); + expect(packageContent).toContain("0.0.1"); + expect(packageContent).toBe('{\n "name": "test-sdk",\n "version": "0.0.1"\n}'); + + const versionContent = await fs.readFile(versionTs, "utf-8"); + expect(versionContent).not.toContain("505.503.4455"); + expect(versionContent).toContain("0.0.1"); + } finally { + await fs.rm(tempDir, { recursive: true, force: true }); + } + }); + + // --- Tests for extractPreviousVersion: edge cases --- + + it("testExtractPreviousVersion_magicVersionInDeletedLine_throws", () => { + // Magic version appears only in a deleted line (not added), should not count + const diff = + "diff --git a/package.json b/package.json\n" + + "index abc123..def456 100644\n" + + "--- a/package.json\n" + + "+++ b/package.json\n" + + "@@ -1,5 +1,4 @@\n" + + " {\n" + + '- "version": "505.503.4455",\n' + + ' "name": "test"\n' + + " }\n"; + + expect(() => + new AutoVersioningService({ logger: mockLogger }).extractPreviousVersion(diff, "505.503.4455") + ).toThrow(AutoVersioningException); + }); + + it("testExtractPreviousVersion_multipleExistingFilesWithVersions_returnsFirst", () => { + // Multiple files with version pairs - should return the first match + const diff = + "diff --git a/package.json b/package.json\n" + + "index abc123..def456 100644\n" + + "--- a/package.json\n" + + "+++ b/package.json\n" + + "@@ -1,5 +1,5 @@\n" + + " {\n" + + '- "version": "3.2.1",\n' + + '+ "version": "505.503.4455",\n' + + ' "name": "test"\n' + + " }\n" + + "diff --git a/src/version.ts b/src/version.ts\n" + + "index 111111..222222 100644\n" + + "--- a/src/version.ts\n" + + "+++ b/src/version.ts\n" + + "@@ -1 +1 @@\n" + + '-export const VERSION = "3.2.1";\n' + + '+export const VERSION = "505.503.4455";\n'; + + const previousVersion = new AutoVersioningService({ logger: mockLogger }).extractPreviousVersion( + diff, + "505.503.4455" + ); + expect(previousVersion).toBe("3.2.1"); + }); + + it("testExtractPreviousVersion_versionWithBuildMetadata", () => { + const diff = + "diff --git a/setup.py b/setup.py\n" + + "index abc123..def456 100644\n" + + "--- a/setup.py\n" + + "+++ b/setup.py\n" + + "@@ -1,3 +1,3 @@\n" + + " setup(\n" + + "- version='1.0.0+build.123',\n" + + "+ version='505.503.4455',\n" + + " )\n"; + + const previousVersion = new AutoVersioningService({ logger: mockLogger }).extractPreviousVersion( + diff, + "505.503.4455" + ); + expect(previousVersion).toBe("1.0.0+build.123"); + }); + + // --- Tests for cleanDiffForAI: mixed new and existing files --- + + it("testCleanDiffForAI_mixedNewAndExistingFiles_preservesNonVersionContent", () => { + const diff = + "diff --git a/src/newFile.ts b/src/newFile.ts\n" + + "new file mode 100644\n" + + "index 0000000..abc123\n" + + "--- /dev/null\n" + + "+++ b/src/newFile.ts\n" + + "@@ -0,0 +1,3 @@\n" + + "+export function hello() {\n" + + '+ return "505.503.4455";\n' + + "+}\n" + + "diff --git a/src/existing.ts b/src/existing.ts\n" + + "index 111111..222222 100644\n" + + "--- a/src/existing.ts\n" + + "+++ b/src/existing.ts\n" + + "@@ -1,3 +1,4 @@\n" + + " export class Existing {\n" + + "+ newMethod() { return true; }\n" + + " }\n"; + + const cleaned = new AutoVersioningService({ logger: mockLogger }).cleanDiffForAI(diff, "505.503.4455"); + + // Version line removed from new file + expect(cleaned).not.toContain("505.503.4455"); + // Non-version new file content preserved + expect(cleaned).toContain("+export function hello()"); + // Existing file changes preserved + expect(cleaned).toContain("+ newMethod() { return true; }"); + }); + // TODO(tjb9dc): Reenable these tests, need to have them reference the LocalTaskHandler's gitDiff function (or refactor) // it("testAutoVersioningWorkflow_endToEnd", async () => { // const tempDir = await fs.mkdtemp(path.join(require("os").tmpdir(), "test-")); diff --git a/packages/cli/workspace/lazy-fern-workspace/src/utils/validateStructureOfYamlFiles.ts b/packages/cli/workspace/lazy-fern-workspace/src/utils/validateStructureOfYamlFiles.ts index 371e3befb9db..f8948f92ff75 100644 --- a/packages/cli/workspace/lazy-fern-workspace/src/utils/validateStructureOfYamlFiles.ts +++ b/packages/cli/workspace/lazy-fern-workspace/src/utils/validateStructureOfYamlFiles.ts @@ -20,6 +20,16 @@ function asJsonSchema(schema: Record): Parameters[1]; } +/** + * A cached parse result that tracks whether JSON schema validation was performed. + * Entries cached via skipValidation are only returned to callers that also skip + * validation, ensuring that callers requesting full validation always get it. + */ +interface CachedEntry { + value: T; + validated: boolean; +} + /** * Module-level caches for validated/parsed file contents. * Keyed on the raw YAML string (file contents), these caches avoid redundant @@ -27,9 +37,9 @@ function asJsonSchema(schema: Record): Parameters(); -const definitionParsedCache = new Map(); -const packageMarkerParsedCache = new Map(); +const rootApiParsedCache = new Map>(); +const definitionParsedCache = new Map>(); +const packageMarkerParsedCache = new Map>(); export declare namespace validateStructureOfYamlFiles { export type Return = SuccessfulResult | FailedResult; @@ -85,18 +95,19 @@ export function validateStructureOfYamlFiles({ }; if (relativeFilepath === ROOT_API_FILENAME) { - // Check cache first + // Check cache: only use a cached entry if it was fully validated, + // or if the caller is skipping validation anyway. const cached = rootApiParsedCache.get(file.rawContents); - if (cached != null) { + if (cached != null && (cached.validated || skipValidation)) { rootApiFile = { - defaultUrl: cached["default-url"], - contents: cached, + defaultUrl: cached.value["default-url"], + contents: cached.value, rawContents: file.rawContents }; } else if (skipValidation) { // Skip JSON schema validation, just run parseOrThrow const contents = RawSchemas.serialization.RootApiFileSchema.parseOrThrow(parsedFileContents); - rootApiParsedCache.set(file.rawContents, contents); + rootApiParsedCache.set(file.rawContents, { value: contents, validated: false }); rootApiFile = { defaultUrl: contents["default-url"], contents, @@ -106,7 +117,7 @@ export function validateStructureOfYamlFiles({ const result = validateAgainstJsonSchema(parsedFileContents, asJsonSchema(RootApiFileJsonSchema)); if (result.success) { const contents = RawSchemas.serialization.RootApiFileSchema.parseOrThrow(parsedFileContents); - rootApiParsedCache.set(file.rawContents, contents); + rootApiParsedCache.set(file.rawContents, { value: contents, validated: true }); rootApiFile = { defaultUrl: contents["default-url"], contents, @@ -117,17 +128,18 @@ export function validateStructureOfYamlFiles({ } } } else if (path.basename(relativeFilepath) === FERN_PACKAGE_MARKER_FILENAME) { - // Check cache first + // Check cache: only use a cached entry if it was fully validated, + // or if the caller is skipping validation anyway. const cached = packageMarkerParsedCache.get(file.rawContents); - if (cached != null) { + if (cached != null && (cached.validated || skipValidation)) { packageMarkers[relativeFilepath] = { - defaultUrl: typeof cached.export === "object" ? cached.export.url : undefined, - contents: cached, + defaultUrl: typeof cached.value.export === "object" ? cached.value.export.url : undefined, + contents: cached.value, rawContents: file.rawContents }; } else if (skipValidation) { const contents = RawSchemas.serialization.PackageMarkerFileSchema.parseOrThrow(parsedFileContents); - packageMarkerParsedCache.set(file.rawContents, contents); + packageMarkerParsedCache.set(file.rawContents, { value: contents, validated: false }); packageMarkers[relativeFilepath] = { defaultUrl: typeof contents.export === "object" ? contents.export.url : undefined, contents, @@ -137,7 +149,7 @@ export function validateStructureOfYamlFiles({ const result = validateAgainstJsonSchema(parsedFileContents, asJsonSchema(PackageMarkerFileJsonSchema)); if (result.success) { const contents = RawSchemas.serialization.PackageMarkerFileSchema.parseOrThrow(parsedFileContents); - packageMarkerParsedCache.set(file.rawContents, contents); + packageMarkerParsedCache.set(file.rawContents, { value: contents, validated: true }); packageMarkers[relativeFilepath] = { defaultUrl: typeof contents.export === "object" ? contents.export.url : undefined, contents, @@ -148,18 +160,19 @@ export function validateStructureOfYamlFiles({ } } } else { - // Check cache first + // Check cache: only use a cached entry if it was fully validated, + // or if the caller is skipping validation anyway. const cached = definitionParsedCache.get(file.rawContents); - if (cached != null) { + if (cached != null && (cached.validated || skipValidation)) { namesDefinitionFiles[relativeFilepath] = { defaultUrl: undefined, - contents: cached, + contents: cached.value, rawContents: file.rawContents, absoluteFilePath: join(absolutePathToDefinition, relativeFilepath) }; } else if (skipValidation) { const contents = RawSchemas.serialization.DefinitionFileSchema.parseOrThrow(parsedFileContents); - definitionParsedCache.set(file.rawContents, contents); + definitionParsedCache.set(file.rawContents, { value: contents, validated: false }); namesDefinitionFiles[relativeFilepath] = { defaultUrl: undefined, contents, @@ -170,7 +183,7 @@ export function validateStructureOfYamlFiles({ const result = validateAgainstJsonSchema(parsedFileContents, asJsonSchema(DefinitionFileJsonSchema)); if (result.success) { const contents = RawSchemas.serialization.DefinitionFileSchema.parseOrThrow(parsedFileContents); - definitionParsedCache.set(file.rawContents, contents); + definitionParsedCache.set(file.rawContents, { value: contents, validated: true }); namesDefinitionFiles[relativeFilepath] = { defaultUrl: undefined, contents, diff --git a/packages/generator-cli/src/configuration/sdk/core/fetcher/Fetcher.ts b/packages/generator-cli/src/configuration/sdk/core/fetcher/Fetcher.ts index 45cae32b23c1..5e5058a9144f 100644 --- a/packages/generator-cli/src/configuration/sdk/core/fetcher/Fetcher.ts +++ b/packages/generator-cli/src/configuration/sdk/core/fetcher/Fetcher.ts @@ -218,7 +218,13 @@ async function getHeaders(args: Fetcher.Args): Promise { newHeaders.set( "Accept", - args.responseType === "json" ? "application/json" : args.responseType === "text" ? "text/plain" : "*/*", + args.responseType === "json" + ? "application/json" + : args.responseType === "text" + ? "text/plain" + : args.responseType === "sse" + ? "text/event-stream" + : "*/*", ); if (args.body !== undefined && args.contentType != null) { newHeaders.set("Content-Type", args.contentType); diff --git a/packages/generator-cli/src/configuration/sdk/core/schemas/builders/list/list.ts b/packages/generator-cli/src/configuration/sdk/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/packages/generator-cli/src/configuration/sdk/core/schemas/builders/list/list.ts +++ b/packages/generator-cli/src/configuration/sdk/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/packages/generator-cli/src/configuration/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts b/packages/generator-cli/src/configuration/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/packages/generator-cli/src/configuration/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/packages/generator-cli/src/configuration/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/packages/generator-cli/src/configuration/sdk/core/schemas/builders/object/object.ts b/packages/generator-cli/src/configuration/sdk/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/packages/generator-cli/src/configuration/sdk/core/schemas/builders/object/object.ts +++ b/packages/generator-cli/src/configuration/sdk/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/packages/generator-cli/src/configuration/sdk/core/schemas/builders/record/record.ts b/packages/generator-cli/src/configuration/sdk/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/packages/generator-cli/src/configuration/sdk/core/schemas/builders/record/record.ts +++ b/packages/generator-cli/src/configuration/sdk/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/packages/generator-cli/src/configuration/sdk/core/schemas/builders/union/union.ts b/packages/generator-cli/src/configuration/sdk/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/packages/generator-cli/src/configuration/sdk/core/schemas/builders/union/union.ts +++ b/packages/generator-cli/src/configuration/sdk/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/packages/generator-cli/src/configuration/sdk/core/schemas/utils/isPlainObject.ts b/packages/generator-cli/src/configuration/sdk/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/packages/generator-cli/src/configuration/sdk/core/schemas/utils/isPlainObject.ts +++ b/packages/generator-cli/src/configuration/sdk/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/packages/ir-sdk/fern/apis/ir-types-latest/generators.yml b/packages/ir-sdk/fern/apis/ir-types-latest/generators.yml index 186de7bf070f..c712392d6e15 100644 --- a/packages/ir-sdk/fern/apis/ir-types-latest/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-latest/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: local-file-system path: ../../../src/sdk @@ -20,7 +20,7 @@ groups: - dynamic generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm package-name: "@fern-api/dynamic-ir-sdk" @@ -31,7 +31,7 @@ groups: node: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v23/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v23/generators.yml index fa9f46c231b6..6b0a42d5e3d2 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v23/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v23/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 config: includeUtilsOnUnionMembers: true noOptionalProperties: true diff --git a/packages/ir-sdk/fern/apis/ir-types-v24/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v24/generators.yml index 6d210149dd10..1893d1940175 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v24/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v24/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 config: includeUtilsOnUnionMembers: true noOptionalProperties: true diff --git a/packages/ir-sdk/fern/apis/ir-types-v25/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v25/generators.yml index e486218c4cfa..e6a93380340e 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v25/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v25/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 config: includeUtilsOnUnionMembers: true noOptionalProperties: true diff --git a/packages/ir-sdk/fern/apis/ir-types-v26/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v26/generators.yml index a5d22f9ceed2..86d41c0cdf33 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v26/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v26/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 config: includeUtilsOnUnionMembers: true noOptionalProperties: true diff --git a/packages/ir-sdk/fern/apis/ir-types-v27/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v27/generators.yml index ac52d8137798..b0bc4bbf6425 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v27/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v27/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 config: includeUtilsOnUnionMembers: true noOptionalProperties: true diff --git a/packages/ir-sdk/fern/apis/ir-types-v28/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v28/generators.yml index 804389fbf29d..c6c2f045c3fb 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v28/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v28/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 config: includeUtilsOnUnionMembers: true noOptionalProperties: true diff --git a/packages/ir-sdk/fern/apis/ir-types-v29/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v29/generators.yml index 3e883e0f1168..3d87fca69b77 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v29/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v29/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 config: includeUtilsOnUnionMembers: true noOptionalProperties: true diff --git a/packages/ir-sdk/fern/apis/ir-types-v30/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v30/generators.yml index 34d53572df73..97220b985b26 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v30/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v30/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 config: includeUtilsOnUnionMembers: true noOptionalProperties: true diff --git a/packages/ir-sdk/fern/apis/ir-types-v31/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v31/generators.yml index cf2ec186e234..e6b85a8df838 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v31/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v31/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v32/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v32/generators.yml index a4dc0f2df074..76a0106cf46d 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v32/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v32/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: local-file-system path: ../../../src/sdk @@ -17,7 +17,7 @@ groups: sdks: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v33/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v33/generators.yml index eca1a8c0a8c7..952177d4b1b4 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v33/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v33/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com @@ -17,7 +17,7 @@ groups: sdks: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v34/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v34/generators.yml index fd8bf4d100f3..a7b4e114a35b 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v34/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v34/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: local-file-system path: ../../../src/sdk @@ -17,7 +17,7 @@ groups: sdks: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v35/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v35/generators.yml index 2c5ecc4ed322..b83f1b9fe70c 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v35/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v35/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com @@ -17,7 +17,7 @@ groups: sdks: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v36/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v36/generators.yml index ee1571004820..bf35766b03b7 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v36/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v36/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com @@ -17,7 +17,7 @@ groups: sdks: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v37/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v37/generators.yml index b16755830c5c..ee1f2aa44e2f 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v37/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v37/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com @@ -17,7 +17,7 @@ groups: sdks: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v38/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v38/generators.yml index cf5af636a830..54d59b3ab3ff 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v38/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v38/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com @@ -18,7 +18,7 @@ groups: sdks: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v39/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v39/generators.yml index 26bbe9ad5a79..358dae3b65fa 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v39/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v39/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com @@ -18,7 +18,7 @@ groups: sdks: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v40/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v40/generators.yml index ea7b73feede5..199dd3823a70 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v40/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v40/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com @@ -18,7 +18,7 @@ groups: sdks: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v41/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v41/generators.yml index 92e6879947df..b14ab8be91eb 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v41/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v41/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com @@ -18,7 +18,7 @@ groups: sdks: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v42/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v42/generators.yml index 0355186271a3..7c2a6e823727 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v42/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v42/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com @@ -18,7 +18,7 @@ groups: sdks: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v43/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v43/generators.yml index 7aa488982e6e..4d69b28c367e 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v43/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v43/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com @@ -18,7 +18,7 @@ groups: node-sdk: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v44/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v44/generators.yml index 3e41983aa250..f2e9602b05b7 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v44/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v44/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com @@ -18,7 +18,7 @@ groups: node-sdk: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v45/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v45/generators.yml index 33151f6e839d..37ccb8d708d1 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v45/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v45/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com @@ -18,7 +18,7 @@ groups: node-sdk: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v46/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v46/generators.yml index 2f5d6b7c38de..e08d3fd3426a 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v46/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v46/generators.yml @@ -1,8 +1,23 @@ +# yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json +default-group: local groups: + local: + generators: + - name: fernapi/fern-typescript-sdk + version: 3.49.0 + output: + location: local-file-system + path: ../../../src/sdk + config: + outputSourceFiles: true + includeUtilsOnUnionMembers: true + noOptionalProperties: true + noSerdeLayer: false + smart-casing: false node: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v47/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v47/generators.yml index d6fee1b43823..115446e419bf 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v47/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v47/generators.yml @@ -1,8 +1,23 @@ +# yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json +default-group: local groups: + local: + generators: + - name: fernapi/fern-typescript-sdk + version: 3.49.0 + output: + location: local-file-system + path: ../../../src/sdk + config: + outputSourceFiles: true + includeUtilsOnUnionMembers: true + noOptionalProperties: true + noSerdeLayer: false + smart-casing: false node: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v48/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v48/generators.yml index 815ed5437588..6840b2dc7f0d 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v48/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v48/generators.yml @@ -1,8 +1,23 @@ +# yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json +default-group: local groups: + local: + generators: + - name: fernapi/fern-typescript-sdk + version: 3.49.0 + output: + location: local-file-system + path: ../../../src/sdk + config: + outputSourceFiles: true + includeUtilsOnUnionMembers: true + noOptionalProperties: true + noSerdeLayer: false + smart-casing: false node: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v49/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v49/generators.yml index c23bbcda87ce..358235ab2103 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v49/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v49/generators.yml @@ -1,8 +1,23 @@ +# yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json +default-group: local groups: + local: + generators: + - name: fernapi/fern-typescript-sdk + version: 3.49.0 + output: + location: local-file-system + path: ../../../src/sdk + config: + outputSourceFiles: true + includeUtilsOnUnionMembers: true + noOptionalProperties: true + noSerdeLayer: false + smart-casing: false node: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v50/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v50/generators.yml index 8480010df7c0..5bd9b6dff521 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v50/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v50/generators.yml @@ -1,8 +1,23 @@ +# yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json +default-group: local groups: + local: + generators: + - name: fernapi/fern-typescript-sdk + version: 3.49.0 + output: + location: local-file-system + path: ../../../src/sdk + config: + outputSourceFiles: true + includeUtilsOnUnionMembers: true + noOptionalProperties: true + noSerdeLayer: false + smart-casing: false node: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v51/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v51/generators.yml index c6377501325c..ed59f42f2c4f 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v51/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v51/generators.yml @@ -1,8 +1,23 @@ +# yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json +default-group: local groups: + local: + generators: + - name: fernapi/fern-typescript-sdk + version: 3.49.0 + output: + location: local-file-system + path: ../../../src/sdk + config: + outputSourceFiles: true + includeUtilsOnUnionMembers: true + noOptionalProperties: true + noSerdeLayer: false + smart-casing: false node: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v52/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v52/generators.yml index 9e2ef5905b86..523e1abb5825 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v52/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v52/generators.yml @@ -1,8 +1,23 @@ +# yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json +default-group: local groups: + local: + generators: + - name: fernapi/fern-typescript-sdk + version: 3.49.0 + output: + location: local-file-system + path: ../../../src/sdk + config: + outputSourceFiles: true + includeUtilsOnUnionMembers: true + noOptionalProperties: true + noSerdeLayer: false + smart-casing: false node: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v53/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v53/generators.yml index fc04b91b894c..5568efa5537b 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v53/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v53/generators.yml @@ -1,12 +1,25 @@ # yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json default-group: local groups: + local: + generators: + - name: fernapi/fern-typescript-sdk + version: 3.49.0 + output: + location: local-file-system + path: ../../../src/sdk + config: + outputSourceFiles: true + includeUtilsOnUnionMembers: true + noOptionalProperties: true + noSerdeLayer: false + smart-casing: false dynamic-ir: audiences: - dynamic generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm package-name: "@fern-api/dynamic-ir-sdk" @@ -17,7 +30,7 @@ groups: node: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v54/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v54/generators.yml index cb6f33220304..74199f0c7b88 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v54/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v54/generators.yml @@ -1,12 +1,25 @@ # yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json default-group: local groups: + local: + generators: + - name: fernapi/fern-typescript-sdk + version: 3.49.0 + output: + location: local-file-system + path: ../../../src/sdk + config: + outputSourceFiles: true + includeUtilsOnUnionMembers: true + noOptionalProperties: true + noSerdeLayer: false + smart-casing: false dynamic-ir: audiences: - dynamic generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm package-name: "@fern-api/dynamic-ir-sdk" @@ -17,7 +30,7 @@ groups: node: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v55/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v55/generators.yml index 4939632ee3ad..a24472a3ad3e 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v55/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v55/generators.yml @@ -1,12 +1,25 @@ # yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json default-group: local groups: + local: + generators: + - name: fernapi/fern-typescript-sdk + version: 3.49.0 + output: + location: local-file-system + path: ../../../src/sdk + config: + outputSourceFiles: true + includeUtilsOnUnionMembers: true + noOptionalProperties: true + noSerdeLayer: false + smart-casing: false dynamic-ir: audiences: - dynamic generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm package-name: "@fern-api/dynamic-ir-sdk" @@ -17,7 +30,7 @@ groups: node: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v56/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v56/generators.yml index 89268d398166..27393f3099ee 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v56/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v56/generators.yml @@ -1,12 +1,25 @@ # yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json default-group: local groups: + local: + generators: + - name: fernapi/fern-typescript-sdk + version: 3.49.0 + output: + location: local-file-system + path: ../../../src/sdk + config: + outputSourceFiles: true + includeUtilsOnUnionMembers: true + noOptionalProperties: true + noSerdeLayer: false + smart-casing: false dynamic-ir: audiences: - dynamic generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm package-name: "@fern-api/dynamic-ir-sdk" @@ -17,7 +30,7 @@ groups: node: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v57/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v57/generators.yml index 1c6991aa2f74..a805780dd7be 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v57/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v57/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: local-file-system path: ../../../src/sdk @@ -19,7 +19,7 @@ groups: - dynamic generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm package-name: "@fern-api/dynamic-ir-sdk" @@ -30,7 +30,7 @@ groups: node: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v58/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v58/generators.yml index 60768e4ec7e8..14469666104a 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v58/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v58/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: local-file-system path: ../../../src/sdk @@ -19,7 +19,7 @@ groups: - dynamic generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm package-name: "@fern-api/dynamic-ir-sdk" @@ -30,7 +30,7 @@ groups: node: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v59/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v59/generators.yml index 3b22f47be05d..b1a1bcaeaf9b 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v59/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v59/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: local-file-system path: ../../../src/sdk @@ -19,7 +19,7 @@ groups: - dynamic generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm package-name: "@fern-api/dynamic-ir-sdk" @@ -30,7 +30,7 @@ groups: node: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v60/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v60/generators.yml index 4f47228c29aa..42f5fc8bdc98 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v60/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v60/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: local-file-system path: ../../../src/sdk @@ -19,7 +19,7 @@ groups: - dynamic generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm package-name: "@fern-api/dynamic-ir-sdk" @@ -30,7 +30,7 @@ groups: node: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v61/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v61/generators.yml index 0ca4be182fc0..58aac206e8da 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v61/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v61/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: local-file-system path: ../../../src/sdk @@ -19,7 +19,7 @@ groups: - dynamic generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm package-name: "@fern-api/dynamic-ir-sdk" @@ -30,7 +30,7 @@ groups: node: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v62/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v62/generators.yml index 79657cc37cf5..d886bd6911e9 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v62/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v62/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: local-file-system path: ../../../src/sdk @@ -19,7 +19,7 @@ groups: - dynamic generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm package-name: "@fern-api/dynamic-ir-sdk" @@ -30,7 +30,7 @@ groups: node: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/fern/apis/ir-types-v63/generators.yml b/packages/ir-sdk/fern/apis/ir-types-v63/generators.yml index eb3a25a4493f..fc00b642f375 100644 --- a/packages/ir-sdk/fern/apis/ir-types-v63/generators.yml +++ b/packages/ir-sdk/fern/apis/ir-types-v63/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: local-file-system path: ../../../src/sdk @@ -20,7 +20,7 @@ groups: - dynamic generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm package-name: "@fern-api/dynamic-ir-sdk" @@ -32,7 +32,7 @@ groups: node: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.3 + version: 3.49.0 output: location: npm url: npm.buildwithfern.com diff --git a/packages/ir-sdk/src/sdk/core/fetcher/Fetcher.ts b/packages/ir-sdk/src/sdk/core/fetcher/Fetcher.ts index 45cae32b23c1..5e5058a9144f 100644 --- a/packages/ir-sdk/src/sdk/core/fetcher/Fetcher.ts +++ b/packages/ir-sdk/src/sdk/core/fetcher/Fetcher.ts @@ -218,7 +218,13 @@ async function getHeaders(args: Fetcher.Args): Promise { newHeaders.set( "Accept", - args.responseType === "json" ? "application/json" : args.responseType === "text" ? "text/plain" : "*/*", + args.responseType === "json" + ? "application/json" + : args.responseType === "text" + ? "text/plain" + : args.responseType === "sse" + ? "text/event-stream" + : "*/*", ); if (args.body !== undefined && args.contentType != null) { newHeaders.set("Content-Type", args.contentType); diff --git a/packages/ir-sdk/src/sdk/core/schemas/builders/list/list.ts b/packages/ir-sdk/src/sdk/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/packages/ir-sdk/src/sdk/core/schemas/builders/list/list.ts +++ b/packages/ir-sdk/src/sdk/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/packages/ir-sdk/src/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts b/packages/ir-sdk/src/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/packages/ir-sdk/src/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/packages/ir-sdk/src/sdk/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/packages/ir-sdk/src/sdk/core/schemas/builders/object/object.ts b/packages/ir-sdk/src/sdk/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/packages/ir-sdk/src/sdk/core/schemas/builders/object/object.ts +++ b/packages/ir-sdk/src/sdk/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/packages/ir-sdk/src/sdk/core/schemas/builders/record/record.ts b/packages/ir-sdk/src/sdk/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/packages/ir-sdk/src/sdk/core/schemas/builders/record/record.ts +++ b/packages/ir-sdk/src/sdk/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/packages/ir-sdk/src/sdk/core/schemas/builders/union/union.ts b/packages/ir-sdk/src/sdk/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/packages/ir-sdk/src/sdk/core/schemas/builders/union/union.ts +++ b/packages/ir-sdk/src/sdk/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/packages/ir-sdk/src/sdk/core/schemas/utils/isPlainObject.ts b/packages/ir-sdk/src/sdk/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/packages/ir-sdk/src/sdk/core/schemas/utils/isPlainObject.ts +++ b/packages/ir-sdk/src/sdk/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/packages/seed/fern/generators.yml b/packages/seed/fern/generators.yml index 3a082c45cef6..65d92c3da2fc 100644 --- a/packages/seed/fern/generators.yml +++ b/packages/seed/fern/generators.yml @@ -4,7 +4,7 @@ groups: local: generators: - name: fernapi/fern-typescript-sdk - version: 3.46.2 + version: 3.49.0 output: location: local-file-system path: ../src/config diff --git a/packages/seed/src/config/api/resources/config/types/BuildScripts.ts b/packages/seed/src/config/api/resources/config/types/BuildScripts.ts index f44ea4ee0ec7..4e465efbb077 100644 --- a/packages/seed/src/config/api/resources/config/types/BuildScripts.ts +++ b/packages/seed/src/config/api/resources/config/types/BuildScripts.ts @@ -6,8 +6,8 @@ import type * as FernSeedConfig from "../../../index.js"; * Scripts pertaining to the artifact produced by this generator and how to build them (e.g. yarn install && yarn build for the TS SDK) */ export interface BuildScripts { - preInstallScript?: FernSeedConfig.Script; - installScript?: FernSeedConfig.Script; - compileScript?: FernSeedConfig.Script; - testScript?: FernSeedConfig.Script; + preInstallScript?: FernSeedConfig.Script | undefined; + installScript?: FernSeedConfig.Script | undefined; + compileScript?: FernSeedConfig.Script | undefined; + testScript?: FernSeedConfig.Script | undefined; } diff --git a/packages/seed/src/config/api/resources/config/types/CliSeedWorkspaceConfiguration.ts b/packages/seed/src/config/api/resources/config/types/CliSeedWorkspaceConfiguration.ts index 0e9d97d92f84..c515088e2c51 100644 --- a/packages/seed/src/config/api/resources/config/types/CliSeedWorkspaceConfiguration.ts +++ b/packages/seed/src/config/api/resources/config/types/CliSeedWorkspaceConfiguration.ts @@ -7,5 +7,5 @@ export interface CliSeedWorkspaceConfiguration { publishRc: FernSeedConfig.PublishCommand; publishDev: FernSeedConfig.PublishCommand; /** The location of the changelog file, the schema of which must follow FDR's `GeneratorReleaseRequest` object. */ - changelogLocation?: string; + changelogLocation?: string | undefined; } diff --git a/packages/seed/src/config/api/resources/config/types/FixtureConfigurations.ts b/packages/seed/src/config/api/resources/config/types/FixtureConfigurations.ts index a65acb3963c8..3b05154c04ee 100644 --- a/packages/seed/src/config/api/resources/config/types/FixtureConfigurations.ts +++ b/packages/seed/src/config/api/resources/config/types/FixtureConfigurations.ts @@ -3,25 +3,25 @@ import type * as FernSeedConfig from "../../../index.js"; export interface FixtureConfigurations { - publishConfig?: unknown; - publishMetadata?: unknown; - customConfig?: unknown; - readmeConfig?: FernSeedConfig.ReadmeConfig; - audiences?: string[]; + publishConfig?: unknown | undefined; + publishMetadata?: unknown | undefined; + customConfig?: unknown | undefined; + readmeConfig?: FernSeedConfig.ReadmeConfig | undefined; + audiences?: string[] | undefined; outputFolder: string; - outputVersion?: string; + outputVersion?: string | undefined; /** * If true, dynamic snippet tests will not be generated for this fixture. * This is useful for situations where the main parts of the sdk are working, but the dynamic snippet tests are not. * Disabling the dynamic snippet tests allows the fixture to compile and run tests, surfacing issues when failures there start to occur. */ - disableDynamicSnippetTests?: boolean; + disableDynamicSnippetTests?: boolean | undefined; /** Overrides the default output mode */ - outputMode?: FernSeedConfig.OutputMode; - license?: unknown; + outputMode?: FernSeedConfig.OutputMode | undefined; + license?: unknown | undefined; /** * Either a boolean to skip all scripts, or a list of script names to skip. * When true, all build and test scripts will be skipped for this fixture. */ - skipScripts?: FernSeedConfig.SkipScripts; + skipScripts?: FernSeedConfig.SkipScripts | undefined; } diff --git a/packages/seed/src/config/api/resources/config/types/LocalBuildInfo.ts b/packages/seed/src/config/api/resources/config/types/LocalBuildInfo.ts index 8f4640c2e313..dde84f98769c 100644 --- a/packages/seed/src/config/api/resources/config/types/LocalBuildInfo.ts +++ b/packages/seed/src/config/api/resources/config/types/LocalBuildInfo.ts @@ -14,5 +14,5 @@ export interface LocalBuildInfo { */ runCommand: string; /** Environment variables to set when running the generator locally. */ - env?: Record; + env?: Record | undefined; } diff --git a/packages/seed/src/config/api/resources/config/types/PublishCommand.ts b/packages/seed/src/config/api/resources/config/types/PublishCommand.ts index 429f420d4486..4261c25567dd 100644 --- a/packages/seed/src/config/api/resources/config/types/PublishCommand.ts +++ b/packages/seed/src/config/api/resources/config/types/PublishCommand.ts @@ -8,8 +8,8 @@ import type * as FernSeedConfig from "../../../index.js"; * Commands can be multi-line, we'll run them all! */ export interface PublishCommand { - workingDirectory?: string; + workingDirectory?: string | undefined; /** The string to substitute for the version in the command. ex. `"$VERSION"` */ - versionSubstitution?: string; + versionSubstitution?: string | undefined; command: FernSeedConfig.DockerCommand; } diff --git a/packages/seed/src/config/api/resources/config/types/PublishDocker.ts b/packages/seed/src/config/api/resources/config/types/PublishDocker.ts index 69827e6642eb..30f3be98541c 100644 --- a/packages/seed/src/config/api/resources/config/types/PublishDocker.ts +++ b/packages/seed/src/config/api/resources/config/types/PublishDocker.ts @@ -6,7 +6,7 @@ import type * as FernSeedConfig from "../../../index.js"; * Configuration for publishing from a docker image, assuming a vanilla docker deployment. */ export interface PublishDocker { - workingDirectory?: string; - preBuildCommands?: FernSeedConfig.DockerCommand; + workingDirectory?: string | undefined; + preBuildCommands?: FernSeedConfig.DockerCommand | undefined; docker: FernSeedConfig.PublishDockerConfiguration; } diff --git a/packages/seed/src/config/api/resources/config/types/PublishDockerConfiguration.ts b/packages/seed/src/config/api/resources/config/types/PublishDockerConfiguration.ts index 2508c0a67fe6..22af37e4f0a3 100644 --- a/packages/seed/src/config/api/resources/config/types/PublishDockerConfiguration.ts +++ b/packages/seed/src/config/api/resources/config/types/PublishDockerConfiguration.ts @@ -3,6 +3,6 @@ export interface PublishDockerConfiguration { context: string; image: string; - aliases?: string[]; + aliases?: string[] | undefined; file: string; } diff --git a/packages/seed/src/config/api/resources/config/types/ReadmeConfig.ts b/packages/seed/src/config/api/resources/config/types/ReadmeConfig.ts index 24da7db465af..ea7441b439c5 100644 --- a/packages/seed/src/config/api/resources/config/types/ReadmeConfig.ts +++ b/packages/seed/src/config/api/resources/config/types/ReadmeConfig.ts @@ -3,10 +3,10 @@ import type * as FernSeedConfig from "../../../index.js"; export interface ReadmeConfig { - defaultEndpoint?: FernSeedConfig.ReadmeEndpoint; - bannerLink?: string; - apiReferenceLink?: string; - features?: Record; - apiName?: string; - disabledSections?: string[]; + defaultEndpoint?: FernSeedConfig.ReadmeEndpoint | undefined; + bannerLink?: string | undefined; + apiReferenceLink?: string | undefined; + features?: Record | undefined; + apiName?: string | undefined; + disabledSections?: string[] | undefined; } diff --git a/packages/seed/src/config/api/resources/config/types/ReadmeEndpointObject.ts b/packages/seed/src/config/api/resources/config/types/ReadmeEndpointObject.ts index fc59c8b6a37b..9e284ea2ce13 100644 --- a/packages/seed/src/config/api/resources/config/types/ReadmeEndpointObject.ts +++ b/packages/seed/src/config/api/resources/config/types/ReadmeEndpointObject.ts @@ -3,5 +3,5 @@ export interface ReadmeEndpointObject { method: string; path: string; - stream?: boolean; + stream?: boolean | undefined; } diff --git a/packages/seed/src/config/api/resources/config/types/Script.ts b/packages/seed/src/config/api/resources/config/types/Script.ts index 13997f5b0c32..a24293a4ebb9 100644 --- a/packages/seed/src/config/api/resources/config/types/Script.ts +++ b/packages/seed/src/config/api/resources/config/types/Script.ts @@ -4,6 +4,6 @@ import type * as FernSeedConfig from "../../../index.js"; export interface Script { /** The name of the script. If provided, this enables fixtures to skip certain scripts. */ - name?: string; + name?: string | undefined; commands: FernSeedConfig.ScriptCommands; } diff --git a/packages/seed/src/config/api/resources/config/types/ScriptCommandsByPhase.ts b/packages/seed/src/config/api/resources/config/types/ScriptCommandsByPhase.ts index 54fa9e027fd9..05aeac198de4 100644 --- a/packages/seed/src/config/api/resources/config/types/ScriptCommandsByPhase.ts +++ b/packages/seed/src/config/api/resources/config/types/ScriptCommandsByPhase.ts @@ -5,7 +5,7 @@ */ export interface ScriptCommandsByPhase { /** Commands to run during the build phase */ - build?: string[]; + build?: string[] | undefined; /** Commands to run during the test phase */ - test?: string[]; + test?: string[] | undefined; } diff --git a/packages/seed/src/config/api/resources/config/types/SeedWorkspaceConfiguration.ts b/packages/seed/src/config/api/resources/config/types/SeedWorkspaceConfiguration.ts index c0bed138fadb..106e42557b2d 100644 --- a/packages/seed/src/config/api/resources/config/types/SeedWorkspaceConfiguration.ts +++ b/packages/seed/src/config/api/resources/config/types/SeedWorkspaceConfiguration.ts @@ -4,26 +4,26 @@ import type * as FernSeedConfig from "../../../index.js"; export interface SeedWorkspaceConfiguration { image: string; - imageAliases?: string[]; + imageAliases?: string[] | undefined; displayName: string; irVersion: string; test: FernSeedConfig.TestConfiguration; publish: FernSeedConfig.PublishConfiguration; /** The location of the changelog file, the schema of which must follow FDR's `GeneratorReleaseRequest` object. */ - changelogLocation?: string; - language?: FernSeedConfig.Language; - defaultCustomConfig?: Record; + changelogLocation?: string | undefined; + language?: FernSeedConfig.Language | undefined; + defaultCustomConfig?: Record | undefined; defaultOutputMode: FernSeedConfig.OutputMode; generatorType: FernSeedConfig.GeneratorType; - buildScripts?: FernSeedConfig.BuildScripts; + buildScripts?: FernSeedConfig.BuildScripts | undefined; /** Configuration that will be used for any custom fixture specified by --custom-fixture */ - customFixtureConfig?: FernSeedConfig.FixtureConfigurations; - fixtures?: Record; - scripts?: FernSeedConfig.ContainerScriptConfig[]; + customFixtureConfig?: FernSeedConfig.FixtureConfigurations | undefined; + fixtures?: Record | undefined; + scripts?: FernSeedConfig.ContainerScriptConfig[] | undefined; /** * List any fixtures that are okay to fail. For normal fixtures * just list the fixture name. For configured fixture list {fixture}:{outputFolder}. */ - allowedFailures?: string[]; - features?: FernSeedConfig.GeneratorFeatures; + allowedFailures?: string[] | undefined; + features?: FernSeedConfig.GeneratorFeatures | undefined; } diff --git a/packages/seed/src/config/api/resources/config/types/TestConfiguration.ts b/packages/seed/src/config/api/resources/config/types/TestConfiguration.ts index 3683f75c44bc..40a36c956b8f 100644 --- a/packages/seed/src/config/api/resources/config/types/TestConfiguration.ts +++ b/packages/seed/src/config/api/resources/config/types/TestConfiguration.ts @@ -4,6 +4,6 @@ import type * as FernSeedConfig from "../../../index.js"; export interface TestConfiguration { docker: FernSeedConfig.TestContainerConfiguration; - podman?: FernSeedConfig.TestContainerConfiguration; - local?: FernSeedConfig.LocalBuildInfo; + podman?: FernSeedConfig.TestContainerConfiguration | undefined; + local?: FernSeedConfig.LocalBuildInfo | undefined; } diff --git a/packages/seed/src/config/api/resources/config/types/TestContainerConfiguration.ts b/packages/seed/src/config/api/resources/config/types/TestContainerConfiguration.ts index 7f41d8aa4aee..356c49da5ccd 100644 --- a/packages/seed/src/config/api/resources/config/types/TestContainerConfiguration.ts +++ b/packages/seed/src/config/api/resources/config/types/TestContainerConfiguration.ts @@ -4,5 +4,5 @@ import type * as FernSeedConfig from "../../../index.js"; export interface TestContainerConfiguration { image: string; - command?: FernSeedConfig.DockerCommand; + command?: FernSeedConfig.DockerCommand | undefined; } diff --git a/packages/seed/src/config/api/resources/features/types/FeatureImplementationWithDetails.ts b/packages/seed/src/config/api/resources/features/types/FeatureImplementationWithDetails.ts index b214df5ab788..33b29316dbb9 100644 --- a/packages/seed/src/config/api/resources/features/types/FeatureImplementationWithDetails.ts +++ b/packages/seed/src/config/api/resources/features/types/FeatureImplementationWithDetails.ts @@ -7,5 +7,5 @@ export interface FeatureImplementationWithDetails { * Any useful information about the implementation status, if it's begun, if it's behind * a feature flag, etc. */ - details?: string; + details?: string | undefined; } diff --git a/packages/seed/src/config/core/fetcher/Fetcher.ts b/packages/seed/src/config/core/fetcher/Fetcher.ts index 45cae32b23c1..5e5058a9144f 100644 --- a/packages/seed/src/config/core/fetcher/Fetcher.ts +++ b/packages/seed/src/config/core/fetcher/Fetcher.ts @@ -218,7 +218,13 @@ async function getHeaders(args: Fetcher.Args): Promise { newHeaders.set( "Accept", - args.responseType === "json" ? "application/json" : args.responseType === "text" ? "text/plain" : "*/*", + args.responseType === "json" + ? "application/json" + : args.responseType === "text" + ? "text/plain" + : args.responseType === "sse" + ? "text/event-stream" + : "*/*", ); if (args.body !== undefined && args.contentType != null) { newHeaders.set("Content-Type", args.contentType); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3a0a0f41725a..2ec6bc29c400 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -103,137 +103,140 @@ catalogs: specifier: 0.0.22 version: 0.0.22 '@fern-fern/ir-v23-sdk': - specifier: 0.0.1 - version: 0.0.1 + specifier: 1.0.0 + version: 1.0.0 '@fern-fern/ir-v24-sdk': - specifier: 0.0.1 - version: 0.0.1 + specifier: 1.0.0 + version: 1.0.0 '@fern-fern/ir-v25-sdk': - specifier: 0.0.1 - version: 0.0.1 + specifier: 1.0.0 + version: 1.0.0 '@fern-fern/ir-v26-sdk': - specifier: 0.0.1 - version: 0.0.1 + specifier: 1.0.0 + version: 1.0.0 '@fern-fern/ir-v27-sdk': - specifier: 0.0.1 - version: 0.0.1 + specifier: 1.0.0 + version: 1.0.0 '@fern-fern/ir-v28-sdk': - specifier: 0.0.3 - version: 0.0.3 + specifier: 1.0.0 + version: 1.0.0 '@fern-fern/ir-v29-sdk': - specifier: 0.0.6 - version: 0.0.6 + specifier: 1.0.0 + version: 1.0.0 '@fern-fern/ir-v3-model': specifier: 0.0.4 version: 0.0.4 '@fern-fern/ir-v30-sdk': - specifier: 0.0.1 - version: 0.0.1 - '@fern-fern/ir-v31-sdk': - specifier: 0.0.5 - version: 0.0.5 - '@fern-fern/ir-v32-sdk': specifier: 1.0.0 version: 1.0.0 - '@fern-fern/ir-v33-sdk': + '@fern-fern/ir-v31-sdk': specifier: 1.0.0 version: 1.0.0 + '@fern-fern/ir-v32-sdk': + specifier: 1.0.1 + version: 1.0.1 + '@fern-fern/ir-v33-sdk': + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v34-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v35-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v36-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v37-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v38-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v39-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v4-model': specifier: 0.0.4 version: 0.0.4 '@fern-fern/ir-v40-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v41-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v42-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v43-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v44-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v45-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v46-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v47-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v48-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v49-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v5-model': specifier: 0.0.3 version: 0.0.3 '@fern-fern/ir-v50-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v51-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v52-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v53-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v54-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v55-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v56-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v57-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v58-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 + '@fern-fern/ir-v59-sdk': + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v6-model': specifier: 0.0.33 version: 0.0.33 '@fern-fern/ir-v60-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v61-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.0.1 + version: 1.0.1 '@fern-fern/ir-v62-sdk': - specifier: 1.0.0 - version: 1.0.0 - '@fern-fern/ir-v63-sdk': specifier: 1.0.1 version: 1.0.1 + '@fern-fern/ir-v63-sdk': + specifier: 1.0.2 + version: 1.0.2 '@fern-fern/ir-v7-model': specifier: 0.0.2 version: 0.0.2 @@ -6251,139 +6254,139 @@ importers: version: 0.0.22 '@fern-fern/ir-v23-sdk': specifier: 'catalog:' - version: 0.0.1 + version: 1.0.0 '@fern-fern/ir-v24-sdk': specifier: 'catalog:' - version: 0.0.1 + version: 1.0.0 '@fern-fern/ir-v25-sdk': specifier: 'catalog:' - version: 0.0.1 + version: 1.0.0 '@fern-fern/ir-v26-sdk': specifier: 'catalog:' - version: 0.0.1 + version: 1.0.0 '@fern-fern/ir-v27-sdk': specifier: 'catalog:' - version: 0.0.1 + version: 1.0.0 '@fern-fern/ir-v28-sdk': specifier: 'catalog:' - version: 0.0.3 + version: 1.0.0 '@fern-fern/ir-v29-sdk': specifier: 'catalog:' - version: 0.0.6 + version: 1.0.0 '@fern-fern/ir-v3-model': specifier: 'catalog:' version: 0.0.4 '@fern-fern/ir-v30-sdk': specifier: 'catalog:' - version: 0.0.1 + version: 1.0.0 '@fern-fern/ir-v31-sdk': specifier: 'catalog:' - version: 0.0.5 + version: 1.0.0 '@fern-fern/ir-v32-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v33-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v34-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v35-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v36-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v37-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v38-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v39-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v4-model': specifier: 'catalog:' version: 0.0.4 '@fern-fern/ir-v40-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v41-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v42-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v43-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v44-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v45-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v46-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v47-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v48-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v49-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v5-model': specifier: 'catalog:' version: 0.0.3 '@fern-fern/ir-v50-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v51-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v52-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v53-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v54-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v55-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v56-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v57-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v58-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v59-sdk': - specifier: 1.0.0 - version: 1.0.0 + specifier: 'catalog:' + version: 1.0.1 '@fern-fern/ir-v6-model': specifier: 'catalog:' version: 0.0.33 '@fern-fern/ir-v60-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v61-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v62-sdk': specifier: 'catalog:' - version: 1.0.0 + version: 1.0.1 '@fern-fern/ir-v63-sdk': specifier: 'catalog:' - version: 1.0.1 + version: 1.0.2 '@fern-fern/ir-v7-model': specifier: 'catalog:' version: 0.0.2 @@ -9729,174 +9732,183 @@ packages: '@fern-fern/ir-v22-model@0.0.22': resolution: {integrity: sha512-5u+RnhNKfH/0lYdZCRvx4b8waL8FiaVqwB9L4otRfH/nR6Wjb8006KR35+LQNrhlzkrqgXd7NJ7/0Aedv+1Bbw==} - '@fern-fern/ir-v23-sdk@0.0.1': - resolution: {integrity: sha512-Hjvq2PxCWR/5tQUvLdb47/RFUbPwAHLiq+GslWN+DdAoCnY0GwQt5dnxD9Izv+JhM7w4XbxgPPSPnDc1x8kH7w==} + '@fern-fern/ir-v23-sdk@1.0.0': + resolution: {integrity: sha512-BQu7cnRQzotj5wH+1GcmdgA5BgOptnEAqnYHJIwn1UR371a66nk2U1gVmtlpQ3JOEv6+cHyJCIKoJJz5Fr62hQ==} + engines: {node: '>=18.0.0'} - '@fern-fern/ir-v24-sdk@0.0.1': - resolution: {integrity: sha512-FgqOUTNIHh1uuZ7PaYyziCGiS8qhUAhtGv6FsoTposOVd/CziYgY3OnUHzXId5TO9HyfOb3ycYqBWeSh7R/qAQ==} + '@fern-fern/ir-v24-sdk@1.0.0': + resolution: {integrity: sha512-aQRvOAurqyPNLxzVK98o5YtOzF+FTPqZvvsXAC+G1wHviVFKoQS6x0upP3ashzgEBNlT3AMMRbk9HzqD8kr8sA==} + engines: {node: '>=18.0.0'} - '@fern-fern/ir-v25-sdk@0.0.1': - resolution: {integrity: sha512-Xw4tXo0ChAhmgV7gCFcm1eY5cnPZ7Sf9nlIsKyXTQNB0EJSoKqD/zonhu5CVNxCO2rn8GBggyt1LEYFl2fC85w==} + '@fern-fern/ir-v25-sdk@1.0.0': + resolution: {integrity: sha512-XeX37JvdfJ+7j28TrVX6PLiC8RmCkvnDsHIDsS49D79rrB2F+YzLmGREMUUbYX3oxJ38OF11j7zMjI+K+yLgWA==} + engines: {node: '>=18.0.0'} - '@fern-fern/ir-v26-sdk@0.0.1': - resolution: {integrity: sha512-rWWNvMxrPXOwqtbu2FQoWOM+Tegr4fDDU23bVYa+Js8aeUqOwxCfjo9PQReg3qNEHG5IAvvNqXDh7fiWGSgapw==} + '@fern-fern/ir-v26-sdk@1.0.0': + resolution: {integrity: sha512-yLCnKxquHtbnYEytvoYJrhT/KhVKVupS56mzdL+u0LtDbNoJYjh7Coqm7hoZXxPrBKfP5nZN9usGLZVwy8JwHA==} + engines: {node: '>=18.0.0'} - '@fern-fern/ir-v27-sdk@0.0.1': - resolution: {integrity: sha512-cmqSy0+WhNzvQvihnMnKA1Nz8V60vYKSowB1yRU+qFeFkBVV9o1TQ35CUPKJxlZsxwgJJ/yuMHbRIlrmMqzlOQ==} + '@fern-fern/ir-v27-sdk@1.0.0': + resolution: {integrity: sha512-mf3jsbEY7wrEPai84ZgQ/4u54JINqmW+uTXVW3z1NYbwPK/omJcxDox4ygKlzFjaa4rG858BJ6r/HUttMEbsMA==} + engines: {node: '>=18.0.0'} - '@fern-fern/ir-v28-sdk@0.0.3': - resolution: {integrity: sha512-ihKA4Qtdx1co/mY2u3JABzCBac7kMX/qAYTM095lB8s2kFO3BeKkpnK+2uke+SMX2TihuJxzoDIUmhNS9ORhBA==} + '@fern-fern/ir-v28-sdk@1.0.0': + resolution: {integrity: sha512-9GvFcna8dMvSS+lc7X49Ez6EHjU8VSEjMISdgemKyJcFLyqOdYxotR+ZEMLv3OttF3hIcamgYiyTtvVbc1cbdQ==} + engines: {node: '>=18.0.0'} - '@fern-fern/ir-v29-sdk@0.0.6': - resolution: {integrity: sha512-VpkdJKQES0mtVBIwHDSmlfN5emq9RYK9mOH2mzwBYnLqlAK2E7eDHGf0le9SmYAzVfvg1ot/vXYWztOzriLI7g==} + '@fern-fern/ir-v29-sdk@1.0.0': + resolution: {integrity: sha512-zpsbNQxIou5PuCSws8kXWbzNr0Np2SDIAXogi+leIleiGA/1woXYUpDEx9H4sd1gKVFgZWEe3sQvl2bwRysm7Q==} + engines: {node: '>=18.0.0'} '@fern-fern/ir-v3-model@0.0.4': resolution: {integrity: sha512-K29rw+4l+LLTRaLN6D8GOOWVxTFgbYyuYdbp0h63wl5vYR/oaNynNRDSIbBAufbFy23IE/drM7drk5oBGWfU2A==} - '@fern-fern/ir-v30-sdk@0.0.1': - resolution: {integrity: sha512-/63kSdMznK3a1xn/JczwQcQ4bhISg7S+BrD+RVaYHm11OA9CtQf4+RHt6rq4ONdC8dup5Tn0/UZ2B/fkpMpz/A==} + '@fern-fern/ir-v30-sdk@1.0.0': + resolution: {integrity: sha512-6LsT9FaUPYW3I767lrjZufvbUa7eAwmiSm5JOVlH1rwHorbuq22D/hdCUl69o6mDW5V6RCjvfzDwiYOPpJLMxA==} + engines: {node: '>=18.0.0'} - '@fern-fern/ir-v31-sdk@0.0.5': - resolution: {integrity: sha512-oUhaRB25e6daxlmWqhGTkVyslIaXRdYUKrFdBeA3lii0ORUvqEb3qGfJ19oSVlDSpwAawdSwCNtiqA4FVMshpg==} + '@fern-fern/ir-v31-sdk@1.0.0': + resolution: {integrity: sha512-+VZIHJ1bjBNr2tqPEuPv2llUox3ersgxWaujqOOMom+7hOl/+VGnvLSpLfCZtsWvW+9x+gXhfwBiqFtCu2jueA==} + engines: {node: '>=18.0.0'} - '@fern-fern/ir-v32-sdk@1.0.0': - resolution: {integrity: sha512-FsNZenfylr3N/hb0wqZKMtxOwlFWkLvUxPFH1BDEKlki8qr+XEfE1nfUIfCW/gaUwKkMPoOsxkLmaT/Xmn6+jA==} + '@fern-fern/ir-v32-sdk@1.0.1': + resolution: {integrity: sha512-g6n/sXxeObFKGbm7xCv14/KnXKzQDP0ZQFCM4CwLiraMkL328nRPJqheD9bT7tqB4UtdqLFbJdEh2C3xdmQi9A==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v33-sdk@1.0.0': - resolution: {integrity: sha512-6w2VjLFNIeXKmvBT7+HEpXSFtdSL6eNtIU3/uN61Rvmb0u3s6nQGVjhFmpmsqDMTB3Pb2ugvWGe++bp71D3C2g==} + '@fern-fern/ir-v33-sdk@1.0.1': + resolution: {integrity: sha512-ffvjja5um8ZXLCRY/KKTMiPDNMsxAC0IGSQw5zpfsGY1FJZDmFIJw65CN+EEgcqMIeX9MVNYiuNC3P150YTNmA==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v34-sdk@1.0.0': - resolution: {integrity: sha512-jUoVJRCz11NRPjzdxL/EVaGEFW1auaNtpmqia+O+sE0Hx3F2Amss409ZQWAKQaOD1St1c3vsWmPvzIJnfhz/fg==} + '@fern-fern/ir-v34-sdk@1.0.1': + resolution: {integrity: sha512-vTyfSHpO8Ngd1V4ASf0drOUZu0VIR3ulHIC0bveoHE3S9O+5QVssBXs0LmL6dbdCWMwBic5/L8w0PEibM1GnVA==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v35-sdk@1.0.0': - resolution: {integrity: sha512-Xc2c+/W/UGiCEERCfD5eouWAASy+/TbvFH4SYwB3tbsSpWrHUtfdaprZXEywbos07eShfllnIW+GcAxnMVMHgQ==} + '@fern-fern/ir-v35-sdk@1.0.1': + resolution: {integrity: sha512-Zm3acSCjSrY8QyJm1ywk0qFaaLQ+jmIdrI4Z3KiwTyNRH3jEmr516+pG8zr4PO+HNZMm7Q+9qnjEu2MNQvPegQ==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v36-sdk@1.0.0': - resolution: {integrity: sha512-5URqnApLQK6s26cYBhMPfAqXhv6NApnxUkpTur2BdriqwNSqPeB8c59dZoLiZkgKEhSrdLuJsoFiWlSgxEfPsw==} + '@fern-fern/ir-v36-sdk@1.0.1': + resolution: {integrity: sha512-KYla6CLrQgS1lTvRHVp7vuGgP9lZ1fcohpXHtoqCIin7zOlrAFYRfqRESVJa0afk3T2XWNNfTGHQXV10vslspA==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v37-sdk@1.0.0': - resolution: {integrity: sha512-YQDT7NUAtKG+WCj7u0S0mTQ+lGV4752iaQFG9UY4d0xOjJK1OsKKut9tWIZUS6/ky5CJLGzyQU4Iig7Bn20Hng==} + '@fern-fern/ir-v37-sdk@1.0.1': + resolution: {integrity: sha512-1UjVsHeQbaVGmwvhK/OqCF3ZLipBbbeaFIVRa97+0fIN0hrWs0Q/WsxoXkft9E2X5hmJ4WbryrtrlBehyI2bcw==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v38-sdk@1.0.0': - resolution: {integrity: sha512-TncPxyFnB5hy4cnyWsloFYY+xNWafZUrZwW2MDVG95BGJNxnIcxlq+OTvDJ0Nt2adm5TRfoOzt43fLyKYtkb0A==} + '@fern-fern/ir-v38-sdk@1.0.1': + resolution: {integrity: sha512-folDF8KXTpdhpdyeiW/6Sh72LXcf6N+HEEXsDwBffdjbq97CDo4dTXu8D3qtKH9k2irhBHPomet0WtVkODe/kg==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v39-sdk@1.0.0': - resolution: {integrity: sha512-cMEnZM0nbX588PjRna/B2oljKUwEDenDMi7/vsJmM/U6E48t3b9PsN2bibS/52iWbTusgOB9QfIlnzGW2VcjMA==} + '@fern-fern/ir-v39-sdk@1.0.1': + resolution: {integrity: sha512-E5MLsdq6wn7IQjxYrmTXDIUeSAfWfIoicVsIEefQsl4rB298YDvoGZd0t2MNlI1J70cLDql5ENmAP7i4TVld5Q==} engines: {node: '>=18.0.0'} '@fern-fern/ir-v4-model@0.0.4': resolution: {integrity: sha512-Xc3VO//QbgITVm6FCPUe4mTfNeTdP8t+zC4a0F0fBz/5iL8rhRhYbdrWvfafJdrF2Hi+LtwvbAFAUPKKLqH1ZQ==} - '@fern-fern/ir-v40-sdk@1.0.0': - resolution: {integrity: sha512-XVp0sVikvICy2lI7IFq98AyAAxBkcOgEZmKgiaa1lSIVkxRI/Q1CNmW5USRi8BrOII4P2fGS3BKNRkUt9PQFVA==} + '@fern-fern/ir-v40-sdk@1.0.1': + resolution: {integrity: sha512-0UBVDKoBVG4NO7Zc04Jo1lJfZ0gNu7OP1LrIor1F5/eeYRZUSpFJZFFmtS8UFe73jMlV5LEu5hYDt5l/LozPRQ==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v41-sdk@1.0.0': - resolution: {integrity: sha512-RSVZR/1F2DO0QKLo2dTDpyT0FG+4Y1PSGWzeq7mBYqENmUHepr0KLGkdIFP/2GZSAEzklB4+g9e5vVzX2uiNaQ==} + '@fern-fern/ir-v41-sdk@1.0.1': + resolution: {integrity: sha512-3FcOMDdwDVWFgTAaWgn3iHX5r055Iq5Tk2vOwtUl2IhgXWjc+aTjPbriq/sXH0+OcEME0gggeIa7C2t/myGi2Q==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v42-sdk@1.0.0': - resolution: {integrity: sha512-ncPds3TV4x2slchY8finenHeB6ObwLc8dNVwm2N9ZiHw0fGwrO7V+AgK9yI87DXBirDsfXgs+4HrMymsnz0C6w==} + '@fern-fern/ir-v42-sdk@1.0.1': + resolution: {integrity: sha512-qxouEI9pjJD8QadgXkakdpoBD/gOiNtQAkE7MuYk/klvTPd5iuSSANkNjdPXadsI4vqESItUQrpNMFD+CcaKtg==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v43-sdk@1.0.0': - resolution: {integrity: sha512-0wBV5Wc5lgX3go47lEINcpHNQyTzBzLNr4/s6TfOMSF+tXiegrzrB6ZmXtZOibrMFiMqfrwxYTDCoeWx+oEpig==} + '@fern-fern/ir-v43-sdk@1.0.1': + resolution: {integrity: sha512-DzuHF+duzjlCibZlMYxqOBjb6dl2FVLC8AtZ65RN9u3gZtk4EPm0Em6SfeAORmy5pS0P+zyQdH/SJysoqbACTA==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v44-sdk@1.0.0': - resolution: {integrity: sha512-W+o9yCQhZcRDIaskgkHIHq7ziaJXuSPibrJpnqLf+HUB9Azgra0hc9S1rb8gN2g73/bheI5y/Yg4zBreOkU2ow==} + '@fern-fern/ir-v44-sdk@1.0.1': + resolution: {integrity: sha512-e7adqM2Fb1AFvNEU/pxEgDzuU1iQdfv6LiTBby+xOHaYRcT1WGDZKMSmqFH3BSadEKwTB7RIJMtF76+kkYXBHA==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v45-sdk@1.0.0': - resolution: {integrity: sha512-IjEMZJWQHmRYvQbss7+BmaVzEe1MO8gxkhvjf1pBdgFGAtAD/NEb6Ls/MfWV6cVjf3xzri04C12VO8sswOq4fg==} + '@fern-fern/ir-v45-sdk@1.0.1': + resolution: {integrity: sha512-zivY4kDwk5+dj7w3NF2NVF8Tofm3TJOxcOoL7lL3TfFr8dkHovmWUnrx4OBQjhZjrUo5gG3sQx4LYGiE3NDq3g==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v46-sdk@1.0.0': - resolution: {integrity: sha512-cyIdSpuLCn2orWVfiyA5b0duL3znSdvU8vEStpGwj2bDVvwRmQu0avzmPOb/GZpFpYYQ1zkNvE3+kEYAkN27Tg==} + '@fern-fern/ir-v46-sdk@1.0.1': + resolution: {integrity: sha512-BMueJfgREAhUvlxu5U6INJTqx5I5By8Yxk8lupToRZ4k5rViylM/WbzqXlGHGQdBi0tO6XQ4a71/XlazBW+4EA==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v47-sdk@1.0.0': - resolution: {integrity: sha512-XKKNu/aJd/8HNYaUCZp4HwF5NusTsKHBoU7BRRaukhGrEnj++1fzzvCWPhoil0/chKTHo9H/4HLuq44bINFVbw==} + '@fern-fern/ir-v47-sdk@1.0.1': + resolution: {integrity: sha512-AMClUqorMvPdCO0kpire5UYcS4ShgPgTOMZ+Gmgk+tqW0qL7HIskcjsKueqlIsgxYEee4ew26kCnmx1slEvcJQ==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v48-sdk@1.0.0': - resolution: {integrity: sha512-MMkt5UVgzmmQ37J+Ipc+zxgCyPiLUP12CTNDTylZSjzEdhSiYh7/VDPLqou+IyXCyb8iiStksLricSZdshm//w==} + '@fern-fern/ir-v48-sdk@1.0.1': + resolution: {integrity: sha512-GpmGHPt4eF4WAe5npXIjNvrNbqKf65b2RRII5pygm80hRe5ZAByHkBO3LJKTHRzUilKbo+oUjHl/OQqpi/JR5A==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v49-sdk@1.0.0': - resolution: {integrity: sha512-gu4SS61XEGtXZArQ9soJ1QtKMwHTjHH8/nPyxysK/sNzVZDca2KJzDAuZajie7uulXLlm091CuI+0hoLuVDKsQ==} + '@fern-fern/ir-v49-sdk@1.0.1': + resolution: {integrity: sha512-tpZdV7n8FcnEJxWy4DzplpBe9q97/7MVVw0plDnp+ONs8gxDB+k7y3jUqWTLl+/uh1ZusfThGMOLaMgK7Lj70Q==} engines: {node: '>=18.0.0'} '@fern-fern/ir-v5-model@0.0.3': resolution: {integrity: sha512-X3Lk1p5w+O/isbJ/UdIVo9JCE7jyj8B20l+ffsArqmxPgHwKpFCTVTjHZQVLDEAfQXDDfj89PfjQg5N8TN+aSA==} - '@fern-fern/ir-v50-sdk@1.0.0': - resolution: {integrity: sha512-NfW+JKhhBA+b6rR6P7cng/cjiK9z0/FCt19JLkWK1y3buUYyRcq7jOLt6tU3e63HEb2UUu+HincrnQyod4HP7Q==} + '@fern-fern/ir-v50-sdk@1.0.1': + resolution: {integrity: sha512-7jiGWQ/g9D6DJEIkJ23LM80c4SwEx4bPkIVjSybnMZ2TsMal6zCWEj3yVO/gHTFJba54VHriMY+b+CQKYKsfyQ==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v51-sdk@1.0.0': - resolution: {integrity: sha512-nux8MCK3dWxJZqo6b/9/uuJ1W1Wrg9NnGn2xwyBQV2GrHf4+M3+lNKX+gbqLWTue8VadndpCHX0TBcFbpNyuMw==} + '@fern-fern/ir-v51-sdk@1.0.1': + resolution: {integrity: sha512-g+baTFvfChx6MJqeBETkizwaaNAH9KyGo/F891f4hGOv0tMKtytjDPByEz3VJ3XXe5VZS54z9UAhECJyuhAuGw==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v52-sdk@1.0.0': - resolution: {integrity: sha512-S4OcTM3I75BXv9PTBWi0EXFr/Qj5t6jhUQI41wyFxBt2kfHqqIOuX4fart5ijzlkpP+CWXu9o9OR7lwrmmqWOg==} + '@fern-fern/ir-v52-sdk@1.0.1': + resolution: {integrity: sha512-i4wJd5eZJ6IJUJ6p8MIzWa9vu1P/YJUxbRjmmzENx7ULPM+Ub5oaQf0Q/ckw27Htm9ztmCb5Habmj/hEstHDMQ==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v53-sdk@1.0.0': - resolution: {integrity: sha512-VIsfQjq4kpbBYCe4R0OUVQSYfyUgnVBGkj7mXBDCtpl/3gjAajT0pkhmyLfXxgp2sgnpAd9WWr7eB5wI1jaWFg==} + '@fern-fern/ir-v53-sdk@1.0.1': + resolution: {integrity: sha512-SVNbkGBVgE/PkgurqqdFEYkmYBGNZzD2Qh8wF9nApp1NH2Wipte5zbzngl78snGTk/e9DN6+mYHfgQPzv6Pi4w==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v54-sdk@1.0.0': - resolution: {integrity: sha512-46OJ8j33OGuA6n2YIuFKtG9TxA6IugFN4oDyl5ZV1oWO9jpiTuyf4Vv0JDPg35Z7s28Ih5HTVpMFKYrW9OBdLw==} + '@fern-fern/ir-v54-sdk@1.0.1': + resolution: {integrity: sha512-ZriTIck0NJRBKDkAceqQ73YzLrEbMZFQeBN9ZQDfgz0KriZ2d3T/vgCutv2c02FNOx5K/jPsjfQcugeLuuF31A==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v55-sdk@1.0.0': - resolution: {integrity: sha512-eRXbQOsdWfKRly3TI5AaLmS2dt2ockLY4sM3B1hEhbYx7pezqKMoSDW8o++B3Qa6Ew6CdTyA9zuN1xOPCm9QPg==} + '@fern-fern/ir-v55-sdk@1.0.1': + resolution: {integrity: sha512-4gww+WcIgqSdRZTz5dDUBnC1Nb4C6O3IIhS914KAbmz1KZFNQwF7i/t+AGVqPTFkfagJdOaj1bbeGqZOWXiV+w==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v56-sdk@1.0.0': - resolution: {integrity: sha512-8lqGHr5sL8L/p9EB1YDuDW/makyO1XtIvN3hI2KgwZZZ5ZSblS1Doys6gpn27OzTY+Ad2j5eI2Uk22rXA7CrSw==} + '@fern-fern/ir-v56-sdk@1.0.1': + resolution: {integrity: sha512-0fXq8ISZvfv4Ykt3yHK4HfpTnTGsAvltWvaYBPwIh8nHmF5S9AIcPfMYTqdECrJOwhOhKtPMsrUvjezRAKKGkQ==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v57-sdk@1.0.0': - resolution: {integrity: sha512-GrMtw2L2Ssko5Qd4oBbdk4TcAmgP6Ee8VFOadW6TAeqn1IAPSo63ay4OyyFQikyXePjsd6tGbFbBVMhSc3mbUA==} + '@fern-fern/ir-v57-sdk@1.0.1': + resolution: {integrity: sha512-dW2kYzg1Ve754JeBmgwMhMlk46YSpJPrYN2eXdZ/9RZo476+/dZqrzSn++t56lnc3aOdrg8yFeuSm9B0vQykBQ==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v58-sdk@1.0.0': - resolution: {integrity: sha512-H3XnzFESgDSRF7ept1rbEm3F3gnyc0IxDC5YH03OO0jZt4yT+IS3Lu9hyIBqyXkJWeSby2b7B3w0PtqFBpz6Yw==} + '@fern-fern/ir-v58-sdk@1.0.1': + resolution: {integrity: sha512-U2U1OwrFyviyaTDzRloDzgBw7Ko6TrbqQZqZfavDBl1Y1S20Q1QfZLqJfJcMhYQcntRbXKJeReNjYsSJFaBGsQ==} engines: {node: '>=18.0.0'} '@fern-fern/ir-v59-sdk@0.0.8': resolution: {integrity: sha512-rPk7crlijG0PvgYZ7g2y5EdhSMMohjGJGfGKo0g5BzdBnA4KqD/LHdful62aHuibYFdORjtGy6GHu1nEH5SD3Q==} - '@fern-fern/ir-v59-sdk@1.0.0': - resolution: {integrity: sha512-hcwkt38gu9LF169Q1H9Ss31VaZ9Y2OqOfOZ7qckZhZNphpVaR39cyC3Qryn2JsPgoCd3Hr8kmHgxewysQGY3Wg==} + '@fern-fern/ir-v59-sdk@1.0.1': + resolution: {integrity: sha512-8qEWF6LW+Hr4Ssk8CjnyKcPNnEAKWQYid0sSLHhpsytCHuoywmId3i8/UsCkj2PCYqd+L2zX1xOQ+nATTVxeAQ==} engines: {node: '>=18.0.0'} '@fern-fern/ir-v6-model@0.0.33': resolution: {integrity: sha512-I3KLvdrRzSrh5CcpU0v/Mjn4ywZhzVwhqJC8IDIQiP1yPvsCnjgbTBNwLRG8KdJqHfLOgdt/hfVzjeuFugSlGA==} - '@fern-fern/ir-v60-sdk@1.0.0': - resolution: {integrity: sha512-lNuUjOgouTN+8FWhw2Jijz50ou5sbJnJTH8f3kvlrOtcFJyUxRb7qkK5jz2SSKkOR+dieXV8H61h7ic3l04vWw==} + '@fern-fern/ir-v60-sdk@1.0.1': + resolution: {integrity: sha512-0j4MrX0nR9lU5YVFBZGEkxOKVNH4ScNqCR5Ah4vDHzv1Y0pQJcxus7nz4pwL6gi3WSyLENXqxWumXuoJNtD2LA==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v61-sdk@1.0.0': - resolution: {integrity: sha512-s0Cn2rZMX9Jorg2s7n3DSG6JiPRv8seFsTgCYBUGEAj9lIjNw5Ybi5gYgx926EPjZcUmEUzXvpYxEsETyfRDdg==} + '@fern-fern/ir-v61-sdk@1.0.1': + resolution: {integrity: sha512-Nwpt2gRL2KS7I7e2GOAic7+RmtT66dbsfd4e6vaFl/90daOGY3kjoyugZGEGKkzv3F2f0LAcyyFpDhi0VwfJSg==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v62-sdk@1.0.0': - resolution: {integrity: sha512-DghnyC7wHaRgs6v4vaXMpB/mX5QlAu/2bgcTEBrAAy3rRYbzDomda6r5fK80Am3TqSvDWfRG8QTAU4WBMnLnvg==} + '@fern-fern/ir-v62-sdk@1.0.1': + resolution: {integrity: sha512-9tKmyf5evhBF7DJOD9chAw4YHcfcxwnT0ROFRoZVC0NBroM3O9TV5tzzWEQNhHuyH+X+2KAs7d9vFwG7/rVQ4g==} engines: {node: '>=18.0.0'} - '@fern-fern/ir-v63-sdk@1.0.1': - resolution: {integrity: sha512-vxbqe6p4onAIYMq5iQGeQUXThF2rZNQJIVdBS3ZV4UyCOcsqyMBZpsXFniikkXa9dAT1K2U8l5Um8vWcKkMxbA==} + '@fern-fern/ir-v63-sdk@1.0.2': + resolution: {integrity: sha512-M1P2yW/KKSfAWjzW0jffPelONtBt72AwwmsutM0WVio0W8BI6PlLC9Tf9xbgYt8HBeN0IP7Hh6rQfxdXeWLRtw==} engines: {node: '>=18.0.0'} '@fern-fern/ir-v7-model@0.0.2': @@ -16544,83 +16556,83 @@ snapshots: '@fern-fern/ir-v22-model@0.0.22': {} - '@fern-fern/ir-v23-sdk@0.0.1': {} + '@fern-fern/ir-v23-sdk@1.0.0': {} - '@fern-fern/ir-v24-sdk@0.0.1': {} + '@fern-fern/ir-v24-sdk@1.0.0': {} - '@fern-fern/ir-v25-sdk@0.0.1': {} + '@fern-fern/ir-v25-sdk@1.0.0': {} - '@fern-fern/ir-v26-sdk@0.0.1': {} + '@fern-fern/ir-v26-sdk@1.0.0': {} - '@fern-fern/ir-v27-sdk@0.0.1': {} + '@fern-fern/ir-v27-sdk@1.0.0': {} - '@fern-fern/ir-v28-sdk@0.0.3': {} + '@fern-fern/ir-v28-sdk@1.0.0': {} - '@fern-fern/ir-v29-sdk@0.0.6': {} + '@fern-fern/ir-v29-sdk@1.0.0': {} '@fern-fern/ir-v3-model@0.0.4': {} - '@fern-fern/ir-v30-sdk@0.0.1': {} + '@fern-fern/ir-v30-sdk@1.0.0': {} - '@fern-fern/ir-v31-sdk@0.0.5': {} + '@fern-fern/ir-v31-sdk@1.0.0': {} - '@fern-fern/ir-v32-sdk@1.0.0': {} + '@fern-fern/ir-v32-sdk@1.0.1': {} - '@fern-fern/ir-v33-sdk@1.0.0': {} + '@fern-fern/ir-v33-sdk@1.0.1': {} - '@fern-fern/ir-v34-sdk@1.0.0': {} + '@fern-fern/ir-v34-sdk@1.0.1': {} - '@fern-fern/ir-v35-sdk@1.0.0': {} + '@fern-fern/ir-v35-sdk@1.0.1': {} - '@fern-fern/ir-v36-sdk@1.0.0': {} + '@fern-fern/ir-v36-sdk@1.0.1': {} - '@fern-fern/ir-v37-sdk@1.0.0': {} + '@fern-fern/ir-v37-sdk@1.0.1': {} - '@fern-fern/ir-v38-sdk@1.0.0': {} + '@fern-fern/ir-v38-sdk@1.0.1': {} - '@fern-fern/ir-v39-sdk@1.0.0': {} + '@fern-fern/ir-v39-sdk@1.0.1': {} '@fern-fern/ir-v4-model@0.0.4': {} - '@fern-fern/ir-v40-sdk@1.0.0': {} + '@fern-fern/ir-v40-sdk@1.0.1': {} - '@fern-fern/ir-v41-sdk@1.0.0': {} + '@fern-fern/ir-v41-sdk@1.0.1': {} - '@fern-fern/ir-v42-sdk@1.0.0': {} + '@fern-fern/ir-v42-sdk@1.0.1': {} - '@fern-fern/ir-v43-sdk@1.0.0': {} + '@fern-fern/ir-v43-sdk@1.0.1': {} - '@fern-fern/ir-v44-sdk@1.0.0': {} + '@fern-fern/ir-v44-sdk@1.0.1': {} - '@fern-fern/ir-v45-sdk@1.0.0': {} + '@fern-fern/ir-v45-sdk@1.0.1': {} - '@fern-fern/ir-v46-sdk@1.0.0': {} + '@fern-fern/ir-v46-sdk@1.0.1': {} - '@fern-fern/ir-v47-sdk@1.0.0': {} + '@fern-fern/ir-v47-sdk@1.0.1': {} - '@fern-fern/ir-v48-sdk@1.0.0': {} + '@fern-fern/ir-v48-sdk@1.0.1': {} - '@fern-fern/ir-v49-sdk@1.0.0': {} + '@fern-fern/ir-v49-sdk@1.0.1': {} '@fern-fern/ir-v5-model@0.0.3': {} - '@fern-fern/ir-v50-sdk@1.0.0': {} + '@fern-fern/ir-v50-sdk@1.0.1': {} - '@fern-fern/ir-v51-sdk@1.0.0': {} + '@fern-fern/ir-v51-sdk@1.0.1': {} - '@fern-fern/ir-v52-sdk@1.0.0': {} + '@fern-fern/ir-v52-sdk@1.0.1': {} - '@fern-fern/ir-v53-sdk@1.0.0': {} + '@fern-fern/ir-v53-sdk@1.0.1': {} - '@fern-fern/ir-v54-sdk@1.0.0': {} + '@fern-fern/ir-v54-sdk@1.0.1': {} - '@fern-fern/ir-v55-sdk@1.0.0': {} + '@fern-fern/ir-v55-sdk@1.0.1': {} - '@fern-fern/ir-v56-sdk@1.0.0': {} + '@fern-fern/ir-v56-sdk@1.0.1': {} - '@fern-fern/ir-v57-sdk@1.0.0': {} + '@fern-fern/ir-v57-sdk@1.0.1': {} - '@fern-fern/ir-v58-sdk@1.0.0': {} + '@fern-fern/ir-v58-sdk@1.0.1': {} '@fern-fern/ir-v59-sdk@0.0.8': dependencies: @@ -16632,17 +16644,17 @@ snapshots: transitivePeerDependencies: - encoding - '@fern-fern/ir-v59-sdk@1.0.0': {} + '@fern-fern/ir-v59-sdk@1.0.1': {} '@fern-fern/ir-v6-model@0.0.33': {} - '@fern-fern/ir-v60-sdk@1.0.0': {} + '@fern-fern/ir-v60-sdk@1.0.1': {} - '@fern-fern/ir-v61-sdk@1.0.0': {} + '@fern-fern/ir-v61-sdk@1.0.1': {} - '@fern-fern/ir-v62-sdk@1.0.0': {} + '@fern-fern/ir-v62-sdk@1.0.1': {} - '@fern-fern/ir-v63-sdk@1.0.1': {} + '@fern-fern/ir-v63-sdk@1.0.2': {} '@fern-fern/ir-v7-model@0.0.2': {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 942613e57f11..f4661eba8828 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -46,50 +46,51 @@ catalog: "@fern-fern/ir-v20-model": 0.0.2 "@fern-fern/ir-v21-model": 0.0.1 "@fern-fern/ir-v22-model": 0.0.22 - "@fern-fern/ir-v23-sdk": 0.0.1 - "@fern-fern/ir-v24-sdk": 0.0.1 - "@fern-fern/ir-v25-sdk": 0.0.1 - "@fern-fern/ir-v26-sdk": 0.0.1 - "@fern-fern/ir-v27-sdk": 0.0.1 - "@fern-fern/ir-v28-sdk": 0.0.3 - "@fern-fern/ir-v29-sdk": 0.0.6 + "@fern-fern/ir-v23-sdk": 1.0.0 + "@fern-fern/ir-v24-sdk": 1.0.0 + "@fern-fern/ir-v25-sdk": 1.0.0 + "@fern-fern/ir-v26-sdk": 1.0.0 + "@fern-fern/ir-v27-sdk": 1.0.0 + "@fern-fern/ir-v28-sdk": 1.0.0 + "@fern-fern/ir-v29-sdk": 1.0.0 "@fern-fern/ir-v3-model": 0.0.4 - "@fern-fern/ir-v30-sdk": 0.0.1 - "@fern-fern/ir-v31-sdk": 0.0.5 - "@fern-fern/ir-v32-sdk": 1.0.0 - "@fern-fern/ir-v33-sdk": 1.0.0 - "@fern-fern/ir-v34-sdk": 1.0.0 - "@fern-fern/ir-v35-sdk": 1.0.0 - "@fern-fern/ir-v36-sdk": 1.0.0 - "@fern-fern/ir-v37-sdk": 1.0.0 - "@fern-fern/ir-v38-sdk": 1.0.0 - "@fern-fern/ir-v39-sdk": 1.0.0 + "@fern-fern/ir-v30-sdk": 1.0.0 + "@fern-fern/ir-v31-sdk": 1.0.0 + "@fern-fern/ir-v32-sdk": 1.0.1 + "@fern-fern/ir-v33-sdk": 1.0.1 + "@fern-fern/ir-v34-sdk": 1.0.1 + "@fern-fern/ir-v35-sdk": 1.0.1 + "@fern-fern/ir-v36-sdk": 1.0.1 + "@fern-fern/ir-v37-sdk": 1.0.1 + "@fern-fern/ir-v38-sdk": 1.0.1 + "@fern-fern/ir-v39-sdk": 1.0.1 "@fern-fern/ir-v4-model": 0.0.4 - "@fern-fern/ir-v40-sdk": 1.0.0 - "@fern-fern/ir-v41-sdk": 1.0.0 - "@fern-fern/ir-v42-sdk": 1.0.0 - "@fern-fern/ir-v43-sdk": 1.0.0 - "@fern-fern/ir-v44-sdk": 1.0.0 - "@fern-fern/ir-v45-sdk": 1.0.0 - "@fern-fern/ir-v46-sdk": 1.0.0 - "@fern-fern/ir-v47-sdk": 1.0.0 - "@fern-fern/ir-v48-sdk": 1.0.0 - "@fern-fern/ir-v49-sdk": 1.0.0 + "@fern-fern/ir-v40-sdk": 1.0.1 + "@fern-fern/ir-v41-sdk": 1.0.1 + "@fern-fern/ir-v42-sdk": 1.0.1 + "@fern-fern/ir-v43-sdk": 1.0.1 + "@fern-fern/ir-v44-sdk": 1.0.1 + "@fern-fern/ir-v45-sdk": 1.0.1 + "@fern-fern/ir-v46-sdk": 1.0.1 + "@fern-fern/ir-v47-sdk": 1.0.1 + "@fern-fern/ir-v48-sdk": 1.0.1 + "@fern-fern/ir-v49-sdk": 1.0.1 "@fern-fern/ir-v5-model": 0.0.3 - "@fern-fern/ir-v50-sdk": 1.0.0 - "@fern-fern/ir-v51-sdk": 1.0.0 - "@fern-fern/ir-v52-sdk": 1.0.0 - "@fern-fern/ir-v53-sdk": 1.0.0 - "@fern-fern/ir-v54-sdk": 1.0.0 - "@fern-fern/ir-v55-sdk": 1.0.0 - "@fern-fern/ir-v56-sdk": 1.0.0 - "@fern-fern/ir-v57-sdk": 1.0.0 - "@fern-fern/ir-v58-sdk": 1.0.0 + "@fern-fern/ir-v50-sdk": 1.0.1 + "@fern-fern/ir-v51-sdk": 1.0.1 + "@fern-fern/ir-v52-sdk": 1.0.1 + "@fern-fern/ir-v53-sdk": 1.0.1 + "@fern-fern/ir-v54-sdk": 1.0.1 + "@fern-fern/ir-v55-sdk": 1.0.1 + "@fern-fern/ir-v56-sdk": 1.0.1 + "@fern-fern/ir-v57-sdk": 1.0.1 + "@fern-fern/ir-v58-sdk": 1.0.1 + "@fern-fern/ir-v59-sdk": 1.0.1 "@fern-fern/ir-v6-model": 0.0.33 - "@fern-fern/ir-v60-sdk": 1.0.0 - "@fern-fern/ir-v61-sdk": 1.0.0 - "@fern-fern/ir-v62-sdk": 1.0.0 - "@fern-fern/ir-v63-sdk": 1.0.1 + "@fern-fern/ir-v60-sdk": 1.0.1 + "@fern-fern/ir-v61-sdk": 1.0.1 + "@fern-fern/ir-v62-sdk": 1.0.1 + "@fern-fern/ir-v63-sdk": 1.0.2 "@fern-fern/ir-v7-model": 0.0.2 "@fern-fern/ir-v8-model": 0.0.1 "@fern-fern/ir-v9-model": 0.0.1 diff --git a/seed/ts-express/alias-extends/core/schemas/builders/list/list.ts b/seed/ts-express/alias-extends/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/alias-extends/core/schemas/builders/list/list.ts +++ b/seed/ts-express/alias-extends/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/alias-extends/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/alias-extends/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/alias-extends/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/alias-extends/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/alias-extends/core/schemas/builders/object/object.ts b/seed/ts-express/alias-extends/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/alias-extends/core/schemas/builders/object/object.ts +++ b/seed/ts-express/alias-extends/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/alias-extends/core/schemas/builders/record/record.ts b/seed/ts-express/alias-extends/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/alias-extends/core/schemas/builders/record/record.ts +++ b/seed/ts-express/alias-extends/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/alias-extends/core/schemas/builders/union/union.ts b/seed/ts-express/alias-extends/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/alias-extends/core/schemas/builders/union/union.ts +++ b/seed/ts-express/alias-extends/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/alias-extends/core/schemas/utils/isPlainObject.ts b/seed/ts-express/alias-extends/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/alias-extends/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/alias-extends/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/alias/core/schemas/builders/list/list.ts b/seed/ts-express/alias/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/alias/core/schemas/builders/list/list.ts +++ b/seed/ts-express/alias/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/alias/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/alias/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/alias/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/alias/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/alias/core/schemas/builders/object/object.ts b/seed/ts-express/alias/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/alias/core/schemas/builders/object/object.ts +++ b/seed/ts-express/alias/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/alias/core/schemas/builders/record/record.ts b/seed/ts-express/alias/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/alias/core/schemas/builders/record/record.ts +++ b/seed/ts-express/alias/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/alias/core/schemas/builders/union/union.ts b/seed/ts-express/alias/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/alias/core/schemas/builders/union/union.ts +++ b/seed/ts-express/alias/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/alias/core/schemas/utils/isPlainObject.ts b/seed/ts-express/alias/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/alias/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/alias/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/any-auth/core/schemas/builders/list/list.ts b/seed/ts-express/any-auth/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/any-auth/core/schemas/builders/list/list.ts +++ b/seed/ts-express/any-auth/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/any-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/any-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/any-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/any-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/any-auth/core/schemas/builders/object/object.ts b/seed/ts-express/any-auth/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/any-auth/core/schemas/builders/object/object.ts +++ b/seed/ts-express/any-auth/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/any-auth/core/schemas/builders/record/record.ts b/seed/ts-express/any-auth/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/any-auth/core/schemas/builders/record/record.ts +++ b/seed/ts-express/any-auth/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/any-auth/core/schemas/builders/union/union.ts b/seed/ts-express/any-auth/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/any-auth/core/schemas/builders/union/union.ts +++ b/seed/ts-express/any-auth/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/any-auth/core/schemas/utils/isPlainObject.ts b/seed/ts-express/any-auth/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/any-auth/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/any-auth/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/audiences/core/schemas/builders/list/list.ts b/seed/ts-express/audiences/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/audiences/core/schemas/builders/list/list.ts +++ b/seed/ts-express/audiences/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/audiences/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/audiences/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/audiences/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/audiences/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/audiences/core/schemas/builders/object/object.ts b/seed/ts-express/audiences/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/audiences/core/schemas/builders/object/object.ts +++ b/seed/ts-express/audiences/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/audiences/core/schemas/builders/record/record.ts b/seed/ts-express/audiences/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/audiences/core/schemas/builders/record/record.ts +++ b/seed/ts-express/audiences/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/audiences/core/schemas/builders/union/union.ts b/seed/ts-express/audiences/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/audiences/core/schemas/builders/union/union.ts +++ b/seed/ts-express/audiences/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/audiences/core/schemas/utils/isPlainObject.ts b/seed/ts-express/audiences/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/audiences/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/audiences/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/list/list.ts b/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/list/list.ts +++ b/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/object/object.ts b/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/object/object.ts +++ b/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/record/record.ts b/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/record/record.ts +++ b/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/union/union.ts b/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/union/union.ts +++ b/seed/ts-express/basic-auth-environment-variables/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/basic-auth-environment-variables/core/schemas/utils/isPlainObject.ts b/seed/ts-express/basic-auth-environment-variables/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/basic-auth-environment-variables/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/basic-auth-environment-variables/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/basic-auth/core/schemas/builders/list/list.ts b/seed/ts-express/basic-auth/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/basic-auth/core/schemas/builders/list/list.ts +++ b/seed/ts-express/basic-auth/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/basic-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/basic-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/basic-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/basic-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/basic-auth/core/schemas/builders/object/object.ts b/seed/ts-express/basic-auth/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/basic-auth/core/schemas/builders/object/object.ts +++ b/seed/ts-express/basic-auth/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/basic-auth/core/schemas/builders/record/record.ts b/seed/ts-express/basic-auth/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/basic-auth/core/schemas/builders/record/record.ts +++ b/seed/ts-express/basic-auth/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/basic-auth/core/schemas/builders/union/union.ts b/seed/ts-express/basic-auth/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/basic-auth/core/schemas/builders/union/union.ts +++ b/seed/ts-express/basic-auth/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/basic-auth/core/schemas/utils/isPlainObject.ts b/seed/ts-express/basic-auth/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/basic-auth/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/basic-auth/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/list/list.ts b/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/list/list.ts +++ b/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/object/object.ts b/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/object/object.ts +++ b/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/record/record.ts b/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/record/record.ts +++ b/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/union/union.ts b/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/union/union.ts +++ b/seed/ts-express/bearer-token-environment-variable/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/bearer-token-environment-variable/core/schemas/utils/isPlainObject.ts b/seed/ts-express/bearer-token-environment-variable/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/bearer-token-environment-variable/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/bearer-token-environment-variable/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/circular-references-advanced/core/schemas/builders/list/list.ts b/seed/ts-express/circular-references-advanced/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/circular-references-advanced/core/schemas/builders/list/list.ts +++ b/seed/ts-express/circular-references-advanced/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/circular-references-advanced/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/circular-references-advanced/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/circular-references-advanced/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/circular-references-advanced/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/circular-references-advanced/core/schemas/builders/object/object.ts b/seed/ts-express/circular-references-advanced/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/circular-references-advanced/core/schemas/builders/object/object.ts +++ b/seed/ts-express/circular-references-advanced/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/circular-references-advanced/core/schemas/builders/record/record.ts b/seed/ts-express/circular-references-advanced/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/circular-references-advanced/core/schemas/builders/record/record.ts +++ b/seed/ts-express/circular-references-advanced/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/circular-references-advanced/core/schemas/builders/union/union.ts b/seed/ts-express/circular-references-advanced/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/circular-references-advanced/core/schemas/builders/union/union.ts +++ b/seed/ts-express/circular-references-advanced/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/circular-references-advanced/core/schemas/utils/isPlainObject.ts b/seed/ts-express/circular-references-advanced/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/circular-references-advanced/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/circular-references-advanced/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/circular-references/core/schemas/builders/list/list.ts b/seed/ts-express/circular-references/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/circular-references/core/schemas/builders/list/list.ts +++ b/seed/ts-express/circular-references/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/circular-references/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/circular-references/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/circular-references/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/circular-references/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/circular-references/core/schemas/builders/object/object.ts b/seed/ts-express/circular-references/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/circular-references/core/schemas/builders/object/object.ts +++ b/seed/ts-express/circular-references/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/circular-references/core/schemas/builders/record/record.ts b/seed/ts-express/circular-references/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/circular-references/core/schemas/builders/record/record.ts +++ b/seed/ts-express/circular-references/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/circular-references/core/schemas/builders/union/union.ts b/seed/ts-express/circular-references/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/circular-references/core/schemas/builders/union/union.ts +++ b/seed/ts-express/circular-references/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/circular-references/core/schemas/utils/isPlainObject.ts b/seed/ts-express/circular-references/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/circular-references/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/circular-references/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/client-side-params/core/schemas/builders/list/list.ts b/seed/ts-express/client-side-params/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/client-side-params/core/schemas/builders/list/list.ts +++ b/seed/ts-express/client-side-params/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/client-side-params/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/client-side-params/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/client-side-params/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/client-side-params/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/client-side-params/core/schemas/builders/object/object.ts b/seed/ts-express/client-side-params/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/client-side-params/core/schemas/builders/object/object.ts +++ b/seed/ts-express/client-side-params/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/client-side-params/core/schemas/builders/record/record.ts b/seed/ts-express/client-side-params/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/client-side-params/core/schemas/builders/record/record.ts +++ b/seed/ts-express/client-side-params/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/client-side-params/core/schemas/builders/union/union.ts b/seed/ts-express/client-side-params/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/client-side-params/core/schemas/builders/union/union.ts +++ b/seed/ts-express/client-side-params/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/client-side-params/core/schemas/utils/isPlainObject.ts b/seed/ts-express/client-side-params/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/client-side-params/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/client-side-params/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/content-type/core/schemas/builders/list/list.ts b/seed/ts-express/content-type/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/content-type/core/schemas/builders/list/list.ts +++ b/seed/ts-express/content-type/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/content-type/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/content-type/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/content-type/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/content-type/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/content-type/core/schemas/builders/object/object.ts b/seed/ts-express/content-type/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/content-type/core/schemas/builders/object/object.ts +++ b/seed/ts-express/content-type/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/content-type/core/schemas/builders/record/record.ts b/seed/ts-express/content-type/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/content-type/core/schemas/builders/record/record.ts +++ b/seed/ts-express/content-type/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/content-type/core/schemas/builders/union/union.ts b/seed/ts-express/content-type/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/content-type/core/schemas/builders/union/union.ts +++ b/seed/ts-express/content-type/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/content-type/core/schemas/utils/isPlainObject.ts b/seed/ts-express/content-type/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/content-type/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/content-type/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/cross-package-type-names/core/schemas/builders/list/list.ts b/seed/ts-express/cross-package-type-names/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/cross-package-type-names/core/schemas/builders/list/list.ts +++ b/seed/ts-express/cross-package-type-names/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/cross-package-type-names/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/cross-package-type-names/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/cross-package-type-names/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/cross-package-type-names/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/cross-package-type-names/core/schemas/builders/object/object.ts b/seed/ts-express/cross-package-type-names/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/cross-package-type-names/core/schemas/builders/object/object.ts +++ b/seed/ts-express/cross-package-type-names/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/cross-package-type-names/core/schemas/builders/record/record.ts b/seed/ts-express/cross-package-type-names/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/cross-package-type-names/core/schemas/builders/record/record.ts +++ b/seed/ts-express/cross-package-type-names/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/cross-package-type-names/core/schemas/builders/union/union.ts b/seed/ts-express/cross-package-type-names/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/cross-package-type-names/core/schemas/builders/union/union.ts +++ b/seed/ts-express/cross-package-type-names/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/cross-package-type-names/core/schemas/utils/isPlainObject.ts b/seed/ts-express/cross-package-type-names/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/cross-package-type-names/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/cross-package-type-names/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/dollar-string-examples/core/schemas/builders/list/list.ts b/seed/ts-express/dollar-string-examples/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/dollar-string-examples/core/schemas/builders/list/list.ts +++ b/seed/ts-express/dollar-string-examples/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/dollar-string-examples/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/dollar-string-examples/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/dollar-string-examples/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/dollar-string-examples/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/dollar-string-examples/core/schemas/builders/object/object.ts b/seed/ts-express/dollar-string-examples/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/dollar-string-examples/core/schemas/builders/object/object.ts +++ b/seed/ts-express/dollar-string-examples/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/dollar-string-examples/core/schemas/builders/record/record.ts b/seed/ts-express/dollar-string-examples/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/dollar-string-examples/core/schemas/builders/record/record.ts +++ b/seed/ts-express/dollar-string-examples/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/dollar-string-examples/core/schemas/builders/union/union.ts b/seed/ts-express/dollar-string-examples/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/dollar-string-examples/core/schemas/builders/union/union.ts +++ b/seed/ts-express/dollar-string-examples/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/dollar-string-examples/core/schemas/utils/isPlainObject.ts b/seed/ts-express/dollar-string-examples/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/dollar-string-examples/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/dollar-string-examples/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/empty-clients/core/schemas/builders/list/list.ts b/seed/ts-express/empty-clients/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/empty-clients/core/schemas/builders/list/list.ts +++ b/seed/ts-express/empty-clients/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/empty-clients/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/empty-clients/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/empty-clients/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/empty-clients/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/empty-clients/core/schemas/builders/object/object.ts b/seed/ts-express/empty-clients/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/empty-clients/core/schemas/builders/object/object.ts +++ b/seed/ts-express/empty-clients/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/empty-clients/core/schemas/builders/record/record.ts b/seed/ts-express/empty-clients/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/empty-clients/core/schemas/builders/record/record.ts +++ b/seed/ts-express/empty-clients/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/empty-clients/core/schemas/builders/union/union.ts b/seed/ts-express/empty-clients/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/empty-clients/core/schemas/builders/union/union.ts +++ b/seed/ts-express/empty-clients/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/empty-clients/core/schemas/utils/isPlainObject.ts b/seed/ts-express/empty-clients/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/empty-clients/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/empty-clients/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/endpoint-security-auth/core/schemas/builders/list/list.ts b/seed/ts-express/endpoint-security-auth/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/endpoint-security-auth/core/schemas/builders/list/list.ts +++ b/seed/ts-express/endpoint-security-auth/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/endpoint-security-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/endpoint-security-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/endpoint-security-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/endpoint-security-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/endpoint-security-auth/core/schemas/builders/object/object.ts b/seed/ts-express/endpoint-security-auth/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/endpoint-security-auth/core/schemas/builders/object/object.ts +++ b/seed/ts-express/endpoint-security-auth/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/endpoint-security-auth/core/schemas/builders/record/record.ts b/seed/ts-express/endpoint-security-auth/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/endpoint-security-auth/core/schemas/builders/record/record.ts +++ b/seed/ts-express/endpoint-security-auth/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/endpoint-security-auth/core/schemas/builders/union/union.ts b/seed/ts-express/endpoint-security-auth/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/endpoint-security-auth/core/schemas/builders/union/union.ts +++ b/seed/ts-express/endpoint-security-auth/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/endpoint-security-auth/core/schemas/utils/isPlainObject.ts b/seed/ts-express/endpoint-security-auth/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/endpoint-security-auth/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/endpoint-security-auth/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/error-property/core/schemas/builders/list/list.ts b/seed/ts-express/error-property/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/error-property/core/schemas/builders/list/list.ts +++ b/seed/ts-express/error-property/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/error-property/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/error-property/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/error-property/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/error-property/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/error-property/core/schemas/builders/object/object.ts b/seed/ts-express/error-property/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/error-property/core/schemas/builders/object/object.ts +++ b/seed/ts-express/error-property/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/error-property/core/schemas/builders/record/record.ts b/seed/ts-express/error-property/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/error-property/core/schemas/builders/record/record.ts +++ b/seed/ts-express/error-property/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/error-property/core/schemas/builders/union/union.ts b/seed/ts-express/error-property/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/error-property/core/schemas/builders/union/union.ts +++ b/seed/ts-express/error-property/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/error-property/core/schemas/utils/isPlainObject.ts b/seed/ts-express/error-property/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/error-property/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/error-property/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/errors/core/schemas/builders/list/list.ts b/seed/ts-express/errors/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/errors/core/schemas/builders/list/list.ts +++ b/seed/ts-express/errors/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/errors/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/errors/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/errors/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/errors/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/errors/core/schemas/builders/object/object.ts b/seed/ts-express/errors/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/errors/core/schemas/builders/object/object.ts +++ b/seed/ts-express/errors/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/errors/core/schemas/builders/record/record.ts b/seed/ts-express/errors/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/errors/core/schemas/builders/record/record.ts +++ b/seed/ts-express/errors/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/errors/core/schemas/builders/union/union.ts b/seed/ts-express/errors/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/errors/core/schemas/builders/union/union.ts +++ b/seed/ts-express/errors/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/errors/core/schemas/utils/isPlainObject.ts b/seed/ts-express/errors/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/errors/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/errors/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/examples/core/schemas/builders/list/list.ts b/seed/ts-express/examples/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/examples/core/schemas/builders/list/list.ts +++ b/seed/ts-express/examples/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/examples/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/examples/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/examples/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/examples/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/examples/core/schemas/builders/object/object.ts b/seed/ts-express/examples/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/examples/core/schemas/builders/object/object.ts +++ b/seed/ts-express/examples/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/examples/core/schemas/builders/record/record.ts b/seed/ts-express/examples/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/examples/core/schemas/builders/record/record.ts +++ b/seed/ts-express/examples/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/examples/core/schemas/builders/union/union.ts b/seed/ts-express/examples/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/examples/core/schemas/builders/union/union.ts +++ b/seed/ts-express/examples/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/examples/core/schemas/utils/isPlainObject.ts b/seed/ts-express/examples/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/examples/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/examples/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/list/list.ts b/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/list/list.ts +++ b/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/object/object.ts b/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/object/object.ts +++ b/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/record/record.ts b/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/record/record.ts +++ b/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/union/union.ts b/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/union/union.ts +++ b/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/utils/isPlainObject.ts b/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/exhaustive/allow-extra-fields/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/list/list.ts b/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/list/list.ts +++ b/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/object/object.ts b/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/object/object.ts +++ b/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/record/record.ts b/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/record/record.ts +++ b/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/union/union.ts b/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/union/union.ts +++ b/seed/ts-express/exhaustive/no-custom-config/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/exhaustive/no-custom-config/core/schemas/utils/isPlainObject.ts b/seed/ts-express/exhaustive/no-custom-config/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/exhaustive/no-custom-config/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/exhaustive/no-custom-config/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/list/list.ts b/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/list/list.ts +++ b/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/object/object.ts b/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/object/object.ts +++ b/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/record/record.ts b/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/record/record.ts +++ b/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/union/union.ts b/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/union/union.ts +++ b/seed/ts-express/exhaustive/no-optional-properties/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/exhaustive/no-optional-properties/core/schemas/utils/isPlainObject.ts b/seed/ts-express/exhaustive/no-optional-properties/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/exhaustive/no-optional-properties/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/exhaustive/no-optional-properties/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/list/list.ts b/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/list/list.ts +++ b/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/object/object.ts b/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/object/object.ts +++ b/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/record/record.ts b/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/record/record.ts +++ b/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/union/union.ts b/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/union/union.ts +++ b/seed/ts-express/exhaustive/retain-original-casing/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/exhaustive/retain-original-casing/core/schemas/utils/isPlainObject.ts b/seed/ts-express/exhaustive/retain-original-casing/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/exhaustive/retain-original-casing/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/exhaustive/retain-original-casing/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/list/list.ts b/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/list/list.ts +++ b/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/object/object.ts b/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/object/object.ts +++ b/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/record/record.ts b/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/record/record.ts +++ b/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/union/union.ts b/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/union/union.ts +++ b/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/utils/isPlainObject.ts b/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/exhaustive/test-package-path/test-packagePath/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/exhaustive/union-utils/core/schemas/builders/list/list.ts b/seed/ts-express/exhaustive/union-utils/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/exhaustive/union-utils/core/schemas/builders/list/list.ts +++ b/seed/ts-express/exhaustive/union-utils/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/exhaustive/union-utils/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/exhaustive/union-utils/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/exhaustive/union-utils/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/exhaustive/union-utils/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/exhaustive/union-utils/core/schemas/builders/object/object.ts b/seed/ts-express/exhaustive/union-utils/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/exhaustive/union-utils/core/schemas/builders/object/object.ts +++ b/seed/ts-express/exhaustive/union-utils/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/exhaustive/union-utils/core/schemas/builders/record/record.ts b/seed/ts-express/exhaustive/union-utils/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/exhaustive/union-utils/core/schemas/builders/record/record.ts +++ b/seed/ts-express/exhaustive/union-utils/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/exhaustive/union-utils/core/schemas/builders/union/union.ts b/seed/ts-express/exhaustive/union-utils/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/exhaustive/union-utils/core/schemas/builders/union/union.ts +++ b/seed/ts-express/exhaustive/union-utils/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/exhaustive/union-utils/core/schemas/utils/isPlainObject.ts b/seed/ts-express/exhaustive/union-utils/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/exhaustive/union-utils/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/exhaustive/union-utils/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/extends/core/schemas/builders/list/list.ts b/seed/ts-express/extends/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/extends/core/schemas/builders/list/list.ts +++ b/seed/ts-express/extends/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/extends/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/extends/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/extends/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/extends/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/extends/core/schemas/builders/object/object.ts b/seed/ts-express/extends/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/extends/core/schemas/builders/object/object.ts +++ b/seed/ts-express/extends/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/extends/core/schemas/builders/record/record.ts b/seed/ts-express/extends/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/extends/core/schemas/builders/record/record.ts +++ b/seed/ts-express/extends/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/extends/core/schemas/builders/union/union.ts b/seed/ts-express/extends/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/extends/core/schemas/builders/union/union.ts +++ b/seed/ts-express/extends/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/extends/core/schemas/utils/isPlainObject.ts b/seed/ts-express/extends/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/extends/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/extends/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/extra-properties/core/schemas/builders/list/list.ts b/seed/ts-express/extra-properties/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/extra-properties/core/schemas/builders/list/list.ts +++ b/seed/ts-express/extra-properties/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/extra-properties/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/extra-properties/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/extra-properties/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/extra-properties/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/extra-properties/core/schemas/builders/object/object.ts b/seed/ts-express/extra-properties/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/extra-properties/core/schemas/builders/object/object.ts +++ b/seed/ts-express/extra-properties/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/extra-properties/core/schemas/builders/record/record.ts b/seed/ts-express/extra-properties/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/extra-properties/core/schemas/builders/record/record.ts +++ b/seed/ts-express/extra-properties/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/extra-properties/core/schemas/builders/union/union.ts b/seed/ts-express/extra-properties/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/extra-properties/core/schemas/builders/union/union.ts +++ b/seed/ts-express/extra-properties/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/extra-properties/core/schemas/utils/isPlainObject.ts b/seed/ts-express/extra-properties/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/extra-properties/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/extra-properties/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/folders/core/schemas/builders/list/list.ts b/seed/ts-express/folders/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/folders/core/schemas/builders/list/list.ts +++ b/seed/ts-express/folders/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/folders/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/folders/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/folders/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/folders/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/folders/core/schemas/builders/object/object.ts b/seed/ts-express/folders/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/folders/core/schemas/builders/object/object.ts +++ b/seed/ts-express/folders/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/folders/core/schemas/builders/record/record.ts b/seed/ts-express/folders/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/folders/core/schemas/builders/record/record.ts +++ b/seed/ts-express/folders/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/folders/core/schemas/builders/union/union.ts b/seed/ts-express/folders/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/folders/core/schemas/builders/union/union.ts +++ b/seed/ts-express/folders/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/folders/core/schemas/utils/isPlainObject.ts b/seed/ts-express/folders/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/folders/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/folders/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/header-auth-environment-variable/core/schemas/builders/list/list.ts b/seed/ts-express/header-auth-environment-variable/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/header-auth-environment-variable/core/schemas/builders/list/list.ts +++ b/seed/ts-express/header-auth-environment-variable/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/header-auth-environment-variable/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/header-auth-environment-variable/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/header-auth-environment-variable/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/header-auth-environment-variable/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/header-auth-environment-variable/core/schemas/builders/object/object.ts b/seed/ts-express/header-auth-environment-variable/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/header-auth-environment-variable/core/schemas/builders/object/object.ts +++ b/seed/ts-express/header-auth-environment-variable/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/header-auth-environment-variable/core/schemas/builders/record/record.ts b/seed/ts-express/header-auth-environment-variable/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/header-auth-environment-variable/core/schemas/builders/record/record.ts +++ b/seed/ts-express/header-auth-environment-variable/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/header-auth-environment-variable/core/schemas/builders/union/union.ts b/seed/ts-express/header-auth-environment-variable/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/header-auth-environment-variable/core/schemas/builders/union/union.ts +++ b/seed/ts-express/header-auth-environment-variable/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/header-auth-environment-variable/core/schemas/utils/isPlainObject.ts b/seed/ts-express/header-auth-environment-variable/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/header-auth-environment-variable/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/header-auth-environment-variable/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/header-auth/core/schemas/builders/list/list.ts b/seed/ts-express/header-auth/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/header-auth/core/schemas/builders/list/list.ts +++ b/seed/ts-express/header-auth/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/header-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/header-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/header-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/header-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/header-auth/core/schemas/builders/object/object.ts b/seed/ts-express/header-auth/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/header-auth/core/schemas/builders/object/object.ts +++ b/seed/ts-express/header-auth/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/header-auth/core/schemas/builders/record/record.ts b/seed/ts-express/header-auth/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/header-auth/core/schemas/builders/record/record.ts +++ b/seed/ts-express/header-auth/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/header-auth/core/schemas/builders/union/union.ts b/seed/ts-express/header-auth/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/header-auth/core/schemas/builders/union/union.ts +++ b/seed/ts-express/header-auth/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/header-auth/core/schemas/utils/isPlainObject.ts b/seed/ts-express/header-auth/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/header-auth/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/header-auth/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/http-head/core/schemas/builders/list/list.ts b/seed/ts-express/http-head/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/http-head/core/schemas/builders/list/list.ts +++ b/seed/ts-express/http-head/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/http-head/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/http-head/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/http-head/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/http-head/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/http-head/core/schemas/builders/object/object.ts b/seed/ts-express/http-head/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/http-head/core/schemas/builders/object/object.ts +++ b/seed/ts-express/http-head/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/http-head/core/schemas/builders/record/record.ts b/seed/ts-express/http-head/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/http-head/core/schemas/builders/record/record.ts +++ b/seed/ts-express/http-head/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/http-head/core/schemas/builders/union/union.ts b/seed/ts-express/http-head/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/http-head/core/schemas/builders/union/union.ts +++ b/seed/ts-express/http-head/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/http-head/core/schemas/utils/isPlainObject.ts b/seed/ts-express/http-head/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/http-head/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/http-head/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/idempotency-headers/core/schemas/builders/list/list.ts b/seed/ts-express/idempotency-headers/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/idempotency-headers/core/schemas/builders/list/list.ts +++ b/seed/ts-express/idempotency-headers/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/idempotency-headers/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/idempotency-headers/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/idempotency-headers/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/idempotency-headers/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/idempotency-headers/core/schemas/builders/object/object.ts b/seed/ts-express/idempotency-headers/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/idempotency-headers/core/schemas/builders/object/object.ts +++ b/seed/ts-express/idempotency-headers/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/idempotency-headers/core/schemas/builders/record/record.ts b/seed/ts-express/idempotency-headers/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/idempotency-headers/core/schemas/builders/record/record.ts +++ b/seed/ts-express/idempotency-headers/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/idempotency-headers/core/schemas/builders/union/union.ts b/seed/ts-express/idempotency-headers/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/idempotency-headers/core/schemas/builders/union/union.ts +++ b/seed/ts-express/idempotency-headers/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/idempotency-headers/core/schemas/utils/isPlainObject.ts b/seed/ts-express/idempotency-headers/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/idempotency-headers/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/idempotency-headers/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/imdb/no-custom-config/core/schemas/builders/list/list.ts b/seed/ts-express/imdb/no-custom-config/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/imdb/no-custom-config/core/schemas/builders/list/list.ts +++ b/seed/ts-express/imdb/no-custom-config/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/imdb/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/imdb/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/imdb/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/imdb/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/imdb/no-custom-config/core/schemas/builders/object/object.ts b/seed/ts-express/imdb/no-custom-config/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/imdb/no-custom-config/core/schemas/builders/object/object.ts +++ b/seed/ts-express/imdb/no-custom-config/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/imdb/no-custom-config/core/schemas/builders/record/record.ts b/seed/ts-express/imdb/no-custom-config/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/imdb/no-custom-config/core/schemas/builders/record/record.ts +++ b/seed/ts-express/imdb/no-custom-config/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/imdb/no-custom-config/core/schemas/builders/union/union.ts b/seed/ts-express/imdb/no-custom-config/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/imdb/no-custom-config/core/schemas/builders/union/union.ts +++ b/seed/ts-express/imdb/no-custom-config/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/imdb/no-custom-config/core/schemas/utils/isPlainObject.ts b/seed/ts-express/imdb/no-custom-config/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/imdb/no-custom-config/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/imdb/no-custom-config/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/imdb/output-compiled/core/schemas/builders/list/list.js b/seed/ts-express/imdb/output-compiled/core/schemas/builders/list/list.js index 464a6d952304..ac5f089dab78 100644 --- a/seed/ts-express/imdb/output-compiled/core/schemas/builders/list/list.js +++ b/seed/ts-express/imdb/output-compiled/core/schemas/builders/list/list.js @@ -31,24 +31,19 @@ function validateAndTransformArray(value, transformItem) { ], }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); - return maybeValidItems.reduce((acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; + const result = []; + const errors = []; + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); } - const errors = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { + else { errors.push(...item.errors); } - return { - ok: false, - errors, - }; - }, { ok: true, value: [] }); + } + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/imdb/output-compiled/core/schemas/builders/object-like/getObjectLikeUtils.js b/seed/ts-express/imdb/output-compiled/core/schemas/builders/object-like/getObjectLikeUtils.js index 2611c1f16dbe..41a2cbe41016 100644 --- a/seed/ts-express/imdb/output-compiled/core/schemas/builders/object-like/getObjectLikeUtils.js +++ b/seed/ts-express/imdb/output-compiled/core/schemas/builders/object-like/getObjectLikeUtils.js @@ -6,6 +6,8 @@ const filterObject_1 = require("../../utils/filterObject"); const getErrorMessageForIncorrectType_1 = require("../../utils/getErrorMessageForIncorrectType"); const isPlainObject_1 = require("../../utils/isPlainObject"); const index_1 = require("../schema-utils/index"); +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; function getObjectLikeUtils(schema) { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -21,9 +23,14 @@ function withParsedProperties(objectLike, properties) { if (!parsedObject.ok) { return parsedObject; } - const additionalProperties = Object.entries(properties).reduce((processed, [key, value]) => { - return Object.assign(Object.assign({}, processed), { [key]: typeof value === "function" ? value(parsedObject.value) : value }); - }, {}); + const additionalProperties = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key]; + additionalProperties[key] = + typeof value === "function" ? value(parsedObject.value) : value; + } + } return { ok: true, value: Object.assign(Object.assign({}, parsedObject.value), additionalProperties), diff --git a/seed/ts-express/imdb/output-compiled/core/schemas/builders/object/object.js b/seed/ts-express/imdb/output-compiled/core/schemas/builders/object/object.js index 97473ea31095..3ac05b5df1ab 100644 --- a/seed/ts-express/imdb/output-compiled/core/schemas/builders/object/object.js +++ b/seed/ts-express/imdb/output-compiled/core/schemas/builders/object/object.js @@ -13,63 +13,113 @@ const partition_1 = require("../../utils/partition"); const index_1 = require("../object-like/index"); const index_2 = require("../schema-utils/index"); const property_1 = require("./property"); +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; function object(schemas) { - const baseSchema = { - _getRawProperties: () => Object.entries(schemas).map(([parsedKey, propertySchema]) => (0, property_1.isProperty)(propertySchema) ? propertySchema.rawKey : parsedKey), - _getParsedProperties: () => (0, keys_1.keys)(schemas), - parse: (raw, opts) => { - const rawKeyToProperty = {}; - const requiredKeys = []; + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. + let _rawKeyToProperty; + function getRawKeyToProperty() { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of (0, entries_1.entries)(schemas)) { - const rawKey = (0, property_1.isProperty)(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = (0, property_1.isProperty)(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : parsedKey; const valueSchema = (0, property_1.isProperty)(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey, valueSchema, }; - rawKeyToProperty[rawKey] = property; + } + } + return _rawKeyToProperty; + } + let _parseRequiredKeys; + let _jsonRequiredKeys; + let _parseRequiredKeysSet; + let _jsonRequiredKeysSet; + function getParseRequiredKeys() { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of (0, entries_1.entries)(schemas)) { + const rawKey = (0, property_1.isProperty)(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : parsedKey; + const valueSchema = (0, property_1.isProperty)(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + function getJsonRequiredKeys() { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys; + } + function getParseRequiredKeysSet() { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet; + } + function getJsonRequiredKeysSet() { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet; + } + const baseSchema = { + _getRawProperties: () => Object.entries(schemas).map(([parsedKey, propertySchema]) => (0, property_1.isProperty)(propertySchema) ? propertySchema.rawKey : parsedKey), + _getParsedProperties: () => (0, keys_1.keys)(schemas), + parse: (raw, opts) => { + var _a; + const breadcrumbsPrefix = (_a = opts === null || opts === void 0 ? void 0 : opts.breadcrumbsPrefix) !== null && _a !== void 0 ? _a : []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, transform: (propertyValue) => { - var _a; - return property.valueSchema.parse(propertyValue, Object.assign(Object.assign({}, opts), { breadcrumbsPrefix: [...((_a = opts === null || opts === void 0 ? void 0 : opts.breadcrumbsPrefix) !== null && _a !== void 0 ? _a : []), rawKey] })); + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, Object.assign(Object.assign({}, opts), { breadcrumbsPrefix: childBreadcrumbs })); }, }; }, unrecognizedObjectKeys: opts === null || opts === void 0 ? void 0 : opts.unrecognizedObjectKeys, skipValidation: opts === null || opts === void 0 ? void 0 : opts.skipValidation, - breadcrumbsPrefix: opts === null || opts === void 0 ? void 0 : opts.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts === null || opts === void 0 ? void 0 : opts.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys = []; - for (const [parsedKey, schemaOrObjectProperty] of (0, entries_1.entries)(schemas)) { - const valueSchema = (0, property_1.isProperty)(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey); - } - } + var _a; + const breadcrumbsPrefix = (_a = opts === null || opts === void 0 ? void 0 : opts.breadcrumbsPrefix) !== null && _a !== void 0 ? _a : []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: (parsedKey) => { const property = schemas[parsedKey]; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition @@ -80,8 +130,8 @@ function object(schemas) { return { transformedKey: property.rawKey, transform: (propertyValue) => { - var _a; - return property.valueSchema.json(propertyValue, Object.assign(Object.assign({}, opts), { breadcrumbsPrefix: [...((_a = opts === null || opts === void 0 ? void 0 : opts.breadcrumbsPrefix) !== null && _a !== void 0 ? _a : []), parsedKey] })); + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, Object.assign(Object.assign({}, opts), { breadcrumbsPrefix: childBreadcrumbs })); }, }; } @@ -89,15 +139,15 @@ function object(schemas) { return { transformedKey: parsedKey, transform: (propertyValue) => { - var _a; - return property.json(propertyValue, Object.assign(Object.assign({}, opts), { breadcrumbsPrefix: [...((_a = opts === null || opts === void 0 ? void 0 : opts.breadcrumbsPrefix) !== null && _a !== void 0 ? _a : []), parsedKey] })); + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, Object.assign(Object.assign({}, opts), { breadcrumbsPrefix: childBreadcrumbs })); }, }; } }, unrecognizedObjectKeys: opts === null || opts === void 0 ? void 0 : opts.unrecognizedObjectKeys, skipValidation: opts === null || opts === void 0 ? void 0 : opts.skipValidation, - breadcrumbsPrefix: opts === null || opts === void 0 ? void 0 : opts.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts === null || opts === void 0 ? void 0 : opts.omitUndefined, }); }, @@ -105,7 +155,7 @@ function object(schemas) { }; return Object.assign(Object.assign(Object.assign(Object.assign({}, (0, maybeSkipValidation_1.maybeSkipValidation)(baseSchema)), (0, index_2.getSchemaUtils)(baseSchema)), (0, index_1.getObjectLikeUtils)(baseSchema)), getObjectUtils(baseSchema)); } -function validateAndTransformObject({ value, requiredKeys, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, breadcrumbsPrefix = [], }) { +function validateAndTransformObject({ value, requiredKeys, requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, breadcrumbsPrefix = [], }) { if (!(0, isPlainObject_1.isPlainObject)(value)) { return { ok: false, @@ -117,13 +167,21 @@ function validateAndTransformObject({ value, requiredKeys, getProperty, unrecogn ], }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors = []; const transformed = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue); if (value.ok) { transformed[property.transformedKey] = value.value; @@ -149,12 +207,16 @@ function validateAndTransformObject({ value, requiredKeys, getProperty, unrecogn } } } - errors.push(...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - }))); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in value)) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { ok: true, diff --git a/seed/ts-express/imdb/output-compiled/core/schemas/builders/record/record.js b/seed/ts-express/imdb/output-compiled/core/schemas/builders/record/record.js index 773966c688ea..9ea69d74a431 100644 --- a/seed/ts-express/imdb/output-compiled/core/schemas/builders/record/record.js +++ b/seed/ts-express/imdb/output-compiled/core/schemas/builders/record/record.js @@ -2,11 +2,12 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.record = record; const Schema_1 = require("../../Schema"); -const entries_1 = require("../../utils/entries"); const getErrorMessageForIncorrectType_1 = require("../../utils/getErrorMessageForIncorrectType"); const isPlainObject_1 = require("../../utils/isPlainObject"); const maybeSkipValidation_1 = require("../../utils/maybeSkipValidation"); const index_1 = require("../schema-utils/index"); +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; function record(keySchema, valueSchema) { const baseSchema = { parse: (raw, opts) => { @@ -55,11 +56,16 @@ function validateAndTransformRecord({ value, isKeyNumeric, transformKey, transfo ], }; } - return (0, entries_1.entries)(value).reduce((accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; + const result = {}; + const errors = []; + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = value[stringKey]; + if (entryValue === undefined) { + continue; } - const acc = accPromise; let key = stringKey; if (isKeyNumeric) { const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; @@ -68,26 +74,21 @@ function validateAndTransformRecord({ value, isKeyNumeric, transformKey, transfo } } const transformedKey = transformKey(key); - const transformedValue = transformValue(value, key); - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: Object.assign(Object.assign({}, acc.value), { [transformedKey.value]: transformedValue.value }), - }; - } - const errors = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!transformedKey.ok) { - errors.push(...transformedKey.errors); + const transformedValue = transformValue(entryValue, key); + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; } - if (!transformedValue.ok) { - errors.push(...transformedValue.errors); + else { + if (!transformedKey.ok) { + errors.push(...transformedKey.errors); + } + if (!transformedValue.ok) { + errors.push(...transformedValue.errors); + } } - return { - ok: false, - errors, - }; - }, { ok: true, value: {} }); + } + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/imdb/output-compiled/core/schemas/builders/union/union.js b/seed/ts-express/imdb/output-compiled/core/schemas/builders/union/union.js index f608f67ed6f8..24a56120e042 100644 --- a/seed/ts-express/imdb/output-compiled/core/schemas/builders/union/union.js +++ b/seed/ts-express/imdb/output-compiled/core/schemas/builders/union/union.js @@ -1,15 +1,4 @@ "use strict"; -var __rest = (this && this.__rest) || function (s, e) { - var t = {}; - for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) - t[p] = s[p]; - if (s != null && typeof Object.getOwnPropertySymbols === "function") - for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { - if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) - t[p[i]] = s[p[i]]; - } - return t; -}; Object.defineProperty(exports, "__esModule", { value: true }); exports.union = union; const Schema_1 = require("../../Schema"); @@ -20,6 +9,8 @@ const maybeSkipValidation_1 = require("../../utils/maybeSkipValidation"); const index_1 = require("../enum/index"); const object_like_1 = require("../object-like"); const index_2 = require("../schema-utils/index"); +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; function union(discriminant, union) { const rawDiscriminant = typeof discriminant === "string" ? discriminant : discriminant.rawDiscriminant; const parsedDiscriminant = typeof discriminant === "string" @@ -79,7 +70,13 @@ function transformAndValidateUnion({ value, discriminant, transformedDiscriminan ], }; } - const _a = value, _b = discriminant, discriminantValue = _a[_b], additionalProperties = __rest(_a, [typeof _b === "symbol" ? _b : _b + ""]); + const discriminantValue = value[discriminant]; + const additionalProperties = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { ok: false, diff --git a/seed/ts-express/imdb/output-compiled/core/schemas/utils/isPlainObject.js b/seed/ts-express/imdb/output-compiled/core/schemas/utils/isPlainObject.js index 6b270a480ae9..5f1a6a1d015d 100644 --- a/seed/ts-express/imdb/output-compiled/core/schemas/utils/isPlainObject.js +++ b/seed/ts-express/imdb/output-compiled/core/schemas/utils/isPlainObject.js @@ -6,12 +6,10 @@ function isPlainObject(value) { if (typeof value !== "object" || value === null) { return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/imdb/output-src-only/core/schemas/builders/list/list.ts b/seed/ts-express/imdb/output-src-only/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/imdb/output-src-only/core/schemas/builders/list/list.ts +++ b/seed/ts-express/imdb/output-src-only/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/imdb/output-src-only/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/imdb/output-src-only/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/imdb/output-src-only/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/imdb/output-src-only/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/imdb/output-src-only/core/schemas/builders/object/object.ts b/seed/ts-express/imdb/output-src-only/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/imdb/output-src-only/core/schemas/builders/object/object.ts +++ b/seed/ts-express/imdb/output-src-only/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/imdb/output-src-only/core/schemas/builders/record/record.ts b/seed/ts-express/imdb/output-src-only/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/imdb/output-src-only/core/schemas/builders/record/record.ts +++ b/seed/ts-express/imdb/output-src-only/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/imdb/output-src-only/core/schemas/builders/union/union.ts b/seed/ts-express/imdb/output-src-only/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/imdb/output-src-only/core/schemas/builders/union/union.ts +++ b/seed/ts-express/imdb/output-src-only/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/imdb/output-src-only/core/schemas/utils/isPlainObject.ts b/seed/ts-express/imdb/output-src-only/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/imdb/output-src-only/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/imdb/output-src-only/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/list/list.ts b/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/list/list.ts +++ b/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/object/object.ts b/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/object/object.ts +++ b/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/record/record.ts b/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/record/record.ts +++ b/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/union/union.ts b/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/union/union.ts +++ b/seed/ts-express/imdb/skip-request-validation/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/imdb/skip-request-validation/core/schemas/utils/isPlainObject.ts b/seed/ts-express/imdb/skip-request-validation/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/imdb/skip-request-validation/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/imdb/skip-request-validation/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/list/list.ts b/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/list/list.ts +++ b/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/object/object.ts b/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/object/object.ts +++ b/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/record/record.ts b/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/record/record.ts +++ b/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/union/union.ts b/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/union/union.ts +++ b/seed/ts-express/imdb/skip-response-validation/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/imdb/skip-response-validation/core/schemas/utils/isPlainObject.ts b/seed/ts-express/imdb/skip-response-validation/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/imdb/skip-response-validation/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/imdb/skip-response-validation/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/imdb/validation-status-code/core/schemas/builders/list/list.ts b/seed/ts-express/imdb/validation-status-code/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/imdb/validation-status-code/core/schemas/builders/list/list.ts +++ b/seed/ts-express/imdb/validation-status-code/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/imdb/validation-status-code/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/imdb/validation-status-code/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/imdb/validation-status-code/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/imdb/validation-status-code/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/imdb/validation-status-code/core/schemas/builders/object/object.ts b/seed/ts-express/imdb/validation-status-code/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/imdb/validation-status-code/core/schemas/builders/object/object.ts +++ b/seed/ts-express/imdb/validation-status-code/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/imdb/validation-status-code/core/schemas/builders/record/record.ts b/seed/ts-express/imdb/validation-status-code/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/imdb/validation-status-code/core/schemas/builders/record/record.ts +++ b/seed/ts-express/imdb/validation-status-code/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/imdb/validation-status-code/core/schemas/builders/union/union.ts b/seed/ts-express/imdb/validation-status-code/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/imdb/validation-status-code/core/schemas/builders/union/union.ts +++ b/seed/ts-express/imdb/validation-status-code/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/imdb/validation-status-code/core/schemas/utils/isPlainObject.ts b/seed/ts-express/imdb/validation-status-code/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/imdb/validation-status-code/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/imdb/validation-status-code/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/inferred-auth-explicit/core/schemas/builders/list/list.ts b/seed/ts-express/inferred-auth-explicit/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/inferred-auth-explicit/core/schemas/builders/list/list.ts +++ b/seed/ts-express/inferred-auth-explicit/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/inferred-auth-explicit/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/inferred-auth-explicit/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/inferred-auth-explicit/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/inferred-auth-explicit/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/inferred-auth-explicit/core/schemas/builders/object/object.ts b/seed/ts-express/inferred-auth-explicit/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/inferred-auth-explicit/core/schemas/builders/object/object.ts +++ b/seed/ts-express/inferred-auth-explicit/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/inferred-auth-explicit/core/schemas/builders/record/record.ts b/seed/ts-express/inferred-auth-explicit/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/inferred-auth-explicit/core/schemas/builders/record/record.ts +++ b/seed/ts-express/inferred-auth-explicit/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/inferred-auth-explicit/core/schemas/builders/union/union.ts b/seed/ts-express/inferred-auth-explicit/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/inferred-auth-explicit/core/schemas/builders/union/union.ts +++ b/seed/ts-express/inferred-auth-explicit/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/inferred-auth-explicit/core/schemas/utils/isPlainObject.ts b/seed/ts-express/inferred-auth-explicit/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/inferred-auth-explicit/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/inferred-auth-explicit/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/list/list.ts b/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/list/list.ts +++ b/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/object/object.ts b/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/object/object.ts +++ b/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/record/record.ts b/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/record/record.ts +++ b/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/union/union.ts b/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/union/union.ts +++ b/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/utils/isPlainObject.ts b/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/inferred-auth-implicit-api-key/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/list/list.ts b/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/list/list.ts +++ b/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/object/object.ts b/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/object/object.ts +++ b/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/record/record.ts b/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/record/record.ts +++ b/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/union/union.ts b/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/union/union.ts +++ b/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/utils/isPlainObject.ts b/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/inferred-auth-implicit-no-expiry/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/list/list.ts b/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/list/list.ts +++ b/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/object/object.ts b/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/object/object.ts +++ b/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/record/record.ts b/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/record/record.ts +++ b/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/union/union.ts b/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/union/union.ts +++ b/seed/ts-express/inferred-auth-implicit-reference/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/inferred-auth-implicit-reference/core/schemas/utils/isPlainObject.ts b/seed/ts-express/inferred-auth-implicit-reference/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/inferred-auth-implicit-reference/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/inferred-auth-implicit-reference/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/inferred-auth-implicit/core/schemas/builders/list/list.ts b/seed/ts-express/inferred-auth-implicit/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/inferred-auth-implicit/core/schemas/builders/list/list.ts +++ b/seed/ts-express/inferred-auth-implicit/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/inferred-auth-implicit/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/inferred-auth-implicit/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/inferred-auth-implicit/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/inferred-auth-implicit/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/inferred-auth-implicit/core/schemas/builders/object/object.ts b/seed/ts-express/inferred-auth-implicit/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/inferred-auth-implicit/core/schemas/builders/object/object.ts +++ b/seed/ts-express/inferred-auth-implicit/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/inferred-auth-implicit/core/schemas/builders/record/record.ts b/seed/ts-express/inferred-auth-implicit/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/inferred-auth-implicit/core/schemas/builders/record/record.ts +++ b/seed/ts-express/inferred-auth-implicit/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/inferred-auth-implicit/core/schemas/builders/union/union.ts b/seed/ts-express/inferred-auth-implicit/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/inferred-auth-implicit/core/schemas/builders/union/union.ts +++ b/seed/ts-express/inferred-auth-implicit/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/inferred-auth-implicit/core/schemas/utils/isPlainObject.ts b/seed/ts-express/inferred-auth-implicit/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/inferred-auth-implicit/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/inferred-auth-implicit/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/license/core/schemas/builders/list/list.ts b/seed/ts-express/license/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/license/core/schemas/builders/list/list.ts +++ b/seed/ts-express/license/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/license/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/license/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/license/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/license/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/license/core/schemas/builders/object/object.ts b/seed/ts-express/license/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/license/core/schemas/builders/object/object.ts +++ b/seed/ts-express/license/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/license/core/schemas/builders/record/record.ts b/seed/ts-express/license/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/license/core/schemas/builders/record/record.ts +++ b/seed/ts-express/license/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/license/core/schemas/builders/union/union.ts b/seed/ts-express/license/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/license/core/schemas/builders/union/union.ts +++ b/seed/ts-express/license/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/license/core/schemas/utils/isPlainObject.ts b/seed/ts-express/license/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/license/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/license/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/literal/core/schemas/builders/list/list.ts b/seed/ts-express/literal/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/literal/core/schemas/builders/list/list.ts +++ b/seed/ts-express/literal/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/literal/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/literal/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/literal/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/literal/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/literal/core/schemas/builders/object/object.ts b/seed/ts-express/literal/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/literal/core/schemas/builders/object/object.ts +++ b/seed/ts-express/literal/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/literal/core/schemas/builders/record/record.ts b/seed/ts-express/literal/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/literal/core/schemas/builders/record/record.ts +++ b/seed/ts-express/literal/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/literal/core/schemas/builders/union/union.ts b/seed/ts-express/literal/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/literal/core/schemas/builders/union/union.ts +++ b/seed/ts-express/literal/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/literal/core/schemas/utils/isPlainObject.ts b/seed/ts-express/literal/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/literal/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/literal/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/literals-unions/core/schemas/builders/list/list.ts b/seed/ts-express/literals-unions/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/literals-unions/core/schemas/builders/list/list.ts +++ b/seed/ts-express/literals-unions/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/literals-unions/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/literals-unions/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/literals-unions/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/literals-unions/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/literals-unions/core/schemas/builders/object/object.ts b/seed/ts-express/literals-unions/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/literals-unions/core/schemas/builders/object/object.ts +++ b/seed/ts-express/literals-unions/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/literals-unions/core/schemas/builders/record/record.ts b/seed/ts-express/literals-unions/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/literals-unions/core/schemas/builders/record/record.ts +++ b/seed/ts-express/literals-unions/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/literals-unions/core/schemas/builders/union/union.ts b/seed/ts-express/literals-unions/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/literals-unions/core/schemas/builders/union/union.ts +++ b/seed/ts-express/literals-unions/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/literals-unions/core/schemas/utils/isPlainObject.ts b/seed/ts-express/literals-unions/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/literals-unions/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/literals-unions/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/list/list.ts b/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/list/list.ts +++ b/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/object/object.ts b/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/object/object.ts +++ b/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/record/record.ts b/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/record/record.ts +++ b/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/union/union.ts b/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/union/union.ts +++ b/seed/ts-express/mixed-case/no-custom-config/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/mixed-case/no-custom-config/core/schemas/utils/isPlainObject.ts b/seed/ts-express/mixed-case/no-custom-config/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/mixed-case/no-custom-config/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/mixed-case/no-custom-config/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/list/list.ts b/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/list/list.ts +++ b/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/object/object.ts b/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/object/object.ts +++ b/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/record/record.ts b/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/record/record.ts +++ b/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/union/union.ts b/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/union/union.ts +++ b/seed/ts-express/mixed-case/retain-original-casing/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/mixed-case/retain-original-casing/core/schemas/utils/isPlainObject.ts b/seed/ts-express/mixed-case/retain-original-casing/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/mixed-case/retain-original-casing/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/mixed-case/retain-original-casing/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/list/list.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/mixed-file-directory/core/schemas/builders/list/list.ts +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/mixed-file-directory/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/object/object.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/mixed-file-directory/core/schemas/builders/object/object.ts +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/record/record.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/mixed-file-directory/core/schemas/builders/record/record.ts +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/mixed-file-directory/core/schemas/builders/union/union.ts b/seed/ts-express/mixed-file-directory/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/mixed-file-directory/core/schemas/builders/union/union.ts +++ b/seed/ts-express/mixed-file-directory/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/mixed-file-directory/core/schemas/utils/isPlainObject.ts b/seed/ts-express/mixed-file-directory/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/mixed-file-directory/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/mixed-file-directory/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/multi-line-docs/core/schemas/builders/list/list.ts b/seed/ts-express/multi-line-docs/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/multi-line-docs/core/schemas/builders/list/list.ts +++ b/seed/ts-express/multi-line-docs/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/multi-line-docs/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/multi-line-docs/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/multi-line-docs/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/multi-line-docs/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/multi-line-docs/core/schemas/builders/object/object.ts b/seed/ts-express/multi-line-docs/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/multi-line-docs/core/schemas/builders/object/object.ts +++ b/seed/ts-express/multi-line-docs/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/multi-line-docs/core/schemas/builders/record/record.ts b/seed/ts-express/multi-line-docs/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/multi-line-docs/core/schemas/builders/record/record.ts +++ b/seed/ts-express/multi-line-docs/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/multi-line-docs/core/schemas/builders/union/union.ts b/seed/ts-express/multi-line-docs/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/multi-line-docs/core/schemas/builders/union/union.ts +++ b/seed/ts-express/multi-line-docs/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/multi-line-docs/core/schemas/utils/isPlainObject.ts b/seed/ts-express/multi-line-docs/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/multi-line-docs/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/multi-line-docs/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/list/list.ts b/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/list/list.ts +++ b/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/object/object.ts b/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/object/object.ts +++ b/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/record/record.ts b/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/record/record.ts +++ b/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/union/union.ts b/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/union/union.ts +++ b/seed/ts-express/multi-url-environment-no-default/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/multi-url-environment-no-default/core/schemas/utils/isPlainObject.ts b/seed/ts-express/multi-url-environment-no-default/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/multi-url-environment-no-default/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/multi-url-environment-no-default/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/multi-url-environment/core/schemas/builders/list/list.ts b/seed/ts-express/multi-url-environment/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/multi-url-environment/core/schemas/builders/list/list.ts +++ b/seed/ts-express/multi-url-environment/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/multi-url-environment/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/multi-url-environment/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/multi-url-environment/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/multi-url-environment/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/multi-url-environment/core/schemas/builders/object/object.ts b/seed/ts-express/multi-url-environment/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/multi-url-environment/core/schemas/builders/object/object.ts +++ b/seed/ts-express/multi-url-environment/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/multi-url-environment/core/schemas/builders/record/record.ts b/seed/ts-express/multi-url-environment/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/multi-url-environment/core/schemas/builders/record/record.ts +++ b/seed/ts-express/multi-url-environment/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/multi-url-environment/core/schemas/builders/union/union.ts b/seed/ts-express/multi-url-environment/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/multi-url-environment/core/schemas/builders/union/union.ts +++ b/seed/ts-express/multi-url-environment/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/multi-url-environment/core/schemas/utils/isPlainObject.ts b/seed/ts-express/multi-url-environment/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/multi-url-environment/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/multi-url-environment/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/no-environment/core/schemas/builders/list/list.ts b/seed/ts-express/no-environment/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/no-environment/core/schemas/builders/list/list.ts +++ b/seed/ts-express/no-environment/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/no-environment/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/no-environment/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/no-environment/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/no-environment/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/no-environment/core/schemas/builders/object/object.ts b/seed/ts-express/no-environment/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/no-environment/core/schemas/builders/object/object.ts +++ b/seed/ts-express/no-environment/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/no-environment/core/schemas/builders/record/record.ts b/seed/ts-express/no-environment/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/no-environment/core/schemas/builders/record/record.ts +++ b/seed/ts-express/no-environment/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/no-environment/core/schemas/builders/union/union.ts b/seed/ts-express/no-environment/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/no-environment/core/schemas/builders/union/union.ts +++ b/seed/ts-express/no-environment/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/no-environment/core/schemas/utils/isPlainObject.ts b/seed/ts-express/no-environment/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/no-environment/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/no-environment/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/no-retries/core/schemas/builders/list/list.ts b/seed/ts-express/no-retries/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/no-retries/core/schemas/builders/list/list.ts +++ b/seed/ts-express/no-retries/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/no-retries/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/no-retries/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/no-retries/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/no-retries/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/no-retries/core/schemas/builders/object/object.ts b/seed/ts-express/no-retries/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/no-retries/core/schemas/builders/object/object.ts +++ b/seed/ts-express/no-retries/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/no-retries/core/schemas/builders/record/record.ts b/seed/ts-express/no-retries/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/no-retries/core/schemas/builders/record/record.ts +++ b/seed/ts-express/no-retries/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/no-retries/core/schemas/builders/union/union.ts b/seed/ts-express/no-retries/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/no-retries/core/schemas/builders/union/union.ts +++ b/seed/ts-express/no-retries/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/no-retries/core/schemas/utils/isPlainObject.ts b/seed/ts-express/no-retries/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/no-retries/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/no-retries/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/nullable-allof-extends/core/schemas/builders/list/list.ts b/seed/ts-express/nullable-allof-extends/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/nullable-allof-extends/core/schemas/builders/list/list.ts +++ b/seed/ts-express/nullable-allof-extends/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/nullable-allof-extends/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/nullable-allof-extends/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/nullable-allof-extends/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/nullable-allof-extends/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/nullable-allof-extends/core/schemas/builders/object/object.ts b/seed/ts-express/nullable-allof-extends/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/nullable-allof-extends/core/schemas/builders/object/object.ts +++ b/seed/ts-express/nullable-allof-extends/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/nullable-allof-extends/core/schemas/builders/record/record.ts b/seed/ts-express/nullable-allof-extends/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/nullable-allof-extends/core/schemas/builders/record/record.ts +++ b/seed/ts-express/nullable-allof-extends/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/nullable-allof-extends/core/schemas/builders/union/union.ts b/seed/ts-express/nullable-allof-extends/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/nullable-allof-extends/core/schemas/builders/union/union.ts +++ b/seed/ts-express/nullable-allof-extends/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/nullable-allof-extends/core/schemas/utils/isPlainObject.ts b/seed/ts-express/nullable-allof-extends/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/nullable-allof-extends/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/nullable-allof-extends/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/nullable-optional/core/schemas/builders/list/list.ts b/seed/ts-express/nullable-optional/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/nullable-optional/core/schemas/builders/list/list.ts +++ b/seed/ts-express/nullable-optional/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/nullable-optional/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/nullable-optional/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/nullable-optional/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/nullable-optional/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/nullable-optional/core/schemas/builders/object/object.ts b/seed/ts-express/nullable-optional/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/nullable-optional/core/schemas/builders/object/object.ts +++ b/seed/ts-express/nullable-optional/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/nullable-optional/core/schemas/builders/record/record.ts b/seed/ts-express/nullable-optional/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/nullable-optional/core/schemas/builders/record/record.ts +++ b/seed/ts-express/nullable-optional/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/nullable-optional/core/schemas/builders/union/union.ts b/seed/ts-express/nullable-optional/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/nullable-optional/core/schemas/builders/union/union.ts +++ b/seed/ts-express/nullable-optional/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/nullable-optional/core/schemas/utils/isPlainObject.ts b/seed/ts-express/nullable-optional/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/nullable-optional/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/nullable-optional/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/nullable-request-body/core/schemas/builders/list/list.ts b/seed/ts-express/nullable-request-body/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/nullable-request-body/core/schemas/builders/list/list.ts +++ b/seed/ts-express/nullable-request-body/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/nullable-request-body/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/nullable-request-body/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/nullable-request-body/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/nullable-request-body/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/nullable-request-body/core/schemas/builders/object/object.ts b/seed/ts-express/nullable-request-body/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/nullable-request-body/core/schemas/builders/object/object.ts +++ b/seed/ts-express/nullable-request-body/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/nullable-request-body/core/schemas/builders/record/record.ts b/seed/ts-express/nullable-request-body/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/nullable-request-body/core/schemas/builders/record/record.ts +++ b/seed/ts-express/nullable-request-body/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/nullable-request-body/core/schemas/builders/union/union.ts b/seed/ts-express/nullable-request-body/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/nullable-request-body/core/schemas/builders/union/union.ts +++ b/seed/ts-express/nullable-request-body/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/nullable-request-body/core/schemas/utils/isPlainObject.ts b/seed/ts-express/nullable-request-body/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/nullable-request-body/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/nullable-request-body/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/nullable/core/schemas/builders/list/list.ts b/seed/ts-express/nullable/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/nullable/core/schemas/builders/list/list.ts +++ b/seed/ts-express/nullable/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/nullable/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/nullable/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/nullable/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/nullable/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/nullable/core/schemas/builders/object/object.ts b/seed/ts-express/nullable/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/nullable/core/schemas/builders/object/object.ts +++ b/seed/ts-express/nullable/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/nullable/core/schemas/builders/record/record.ts b/seed/ts-express/nullable/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/nullable/core/schemas/builders/record/record.ts +++ b/seed/ts-express/nullable/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/nullable/core/schemas/builders/union/union.ts b/seed/ts-express/nullable/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/nullable/core/schemas/builders/union/union.ts +++ b/seed/ts-express/nullable/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/nullable/core/schemas/utils/isPlainObject.ts b/seed/ts-express/nullable/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/nullable/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/nullable/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/list/list.ts b/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/list/list.ts +++ b/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/object/object.ts b/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/object/object.ts +++ b/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/record/record.ts b/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/record/record.ts +++ b/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/union/union.ts b/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/union/union.ts +++ b/seed/ts-express/oauth-client-credentials-custom/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/oauth-client-credentials-custom/core/schemas/utils/isPlainObject.ts b/seed/ts-express/oauth-client-credentials-custom/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/oauth-client-credentials-custom/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/oauth-client-credentials-custom/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/list/list.ts b/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/list/list.ts +++ b/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/object/object.ts b/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/object/object.ts +++ b/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/record/record.ts b/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/record/record.ts +++ b/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/union/union.ts b/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/union/union.ts +++ b/seed/ts-express/oauth-client-credentials-default/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/oauth-client-credentials-default/core/schemas/utils/isPlainObject.ts b/seed/ts-express/oauth-client-credentials-default/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/oauth-client-credentials-default/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/oauth-client-credentials-default/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/list/list.ts b/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/list/list.ts +++ b/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/object/object.ts b/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/object/object.ts +++ b/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/record/record.ts b/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/record/record.ts +++ b/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/union/union.ts b/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/union/union.ts +++ b/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/utils/isPlainObject.ts b/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/oauth-client-credentials-environment-variables/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/list/list.ts b/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/list/list.ts +++ b/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/object/object.ts b/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/object/object.ts +++ b/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/record/record.ts b/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/record/record.ts +++ b/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/union/union.ts b/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/union/union.ts +++ b/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/utils/isPlainObject.ts b/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/oauth-client-credentials-mandatory-auth/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/list/list.ts b/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/list/list.ts +++ b/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/object/object.ts b/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/object/object.ts +++ b/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/record/record.ts b/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/record/record.ts +++ b/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/union/union.ts b/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/union/union.ts +++ b/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/utils/isPlainObject.ts b/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/oauth-client-credentials-nested-root/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/list/list.ts b/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/list/list.ts +++ b/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/object/object.ts b/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/object/object.ts +++ b/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/record/record.ts b/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/record/record.ts +++ b/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/union/union.ts b/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/union/union.ts +++ b/seed/ts-express/oauth-client-credentials-reference/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/oauth-client-credentials-reference/core/schemas/utils/isPlainObject.ts b/seed/ts-express/oauth-client-credentials-reference/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/oauth-client-credentials-reference/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/oauth-client-credentials-reference/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/list/list.ts b/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/list/list.ts +++ b/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/object/object.ts b/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/object/object.ts +++ b/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/record/record.ts b/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/record/record.ts +++ b/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/union/union.ts b/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/union/union.ts +++ b/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/utils/isPlainObject.ts b/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/oauth-client-credentials-with-variables/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/oauth-client-credentials/core/schemas/builders/list/list.ts b/seed/ts-express/oauth-client-credentials/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/oauth-client-credentials/core/schemas/builders/list/list.ts +++ b/seed/ts-express/oauth-client-credentials/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/oauth-client-credentials/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/oauth-client-credentials/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/oauth-client-credentials/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/oauth-client-credentials/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/oauth-client-credentials/core/schemas/builders/object/object.ts b/seed/ts-express/oauth-client-credentials/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/oauth-client-credentials/core/schemas/builders/object/object.ts +++ b/seed/ts-express/oauth-client-credentials/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/oauth-client-credentials/core/schemas/builders/record/record.ts b/seed/ts-express/oauth-client-credentials/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/oauth-client-credentials/core/schemas/builders/record/record.ts +++ b/seed/ts-express/oauth-client-credentials/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/oauth-client-credentials/core/schemas/builders/union/union.ts b/seed/ts-express/oauth-client-credentials/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/oauth-client-credentials/core/schemas/builders/union/union.ts +++ b/seed/ts-express/oauth-client-credentials/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/oauth-client-credentials/core/schemas/utils/isPlainObject.ts b/seed/ts-express/oauth-client-credentials/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/oauth-client-credentials/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/oauth-client-credentials/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/object/core/schemas/builders/list/list.ts b/seed/ts-express/object/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/object/core/schemas/builders/list/list.ts +++ b/seed/ts-express/object/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/object/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/object/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/object/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/object/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/object/core/schemas/builders/object/object.ts b/seed/ts-express/object/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/object/core/schemas/builders/object/object.ts +++ b/seed/ts-express/object/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/object/core/schemas/builders/record/record.ts b/seed/ts-express/object/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/object/core/schemas/builders/record/record.ts +++ b/seed/ts-express/object/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/object/core/schemas/builders/union/union.ts b/seed/ts-express/object/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/object/core/schemas/builders/union/union.ts +++ b/seed/ts-express/object/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/object/core/schemas/utils/isPlainObject.ts b/seed/ts-express/object/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/object/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/object/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/objects-with-imports/core/schemas/builders/list/list.ts b/seed/ts-express/objects-with-imports/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/objects-with-imports/core/schemas/builders/list/list.ts +++ b/seed/ts-express/objects-with-imports/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/objects-with-imports/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/objects-with-imports/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/objects-with-imports/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/objects-with-imports/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/objects-with-imports/core/schemas/builders/object/object.ts b/seed/ts-express/objects-with-imports/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/objects-with-imports/core/schemas/builders/object/object.ts +++ b/seed/ts-express/objects-with-imports/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/objects-with-imports/core/schemas/builders/record/record.ts b/seed/ts-express/objects-with-imports/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/objects-with-imports/core/schemas/builders/record/record.ts +++ b/seed/ts-express/objects-with-imports/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/objects-with-imports/core/schemas/builders/union/union.ts b/seed/ts-express/objects-with-imports/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/objects-with-imports/core/schemas/builders/union/union.ts +++ b/seed/ts-express/objects-with-imports/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/objects-with-imports/core/schemas/utils/isPlainObject.ts b/seed/ts-express/objects-with-imports/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/objects-with-imports/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/objects-with-imports/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/optional/core/schemas/builders/list/list.ts b/seed/ts-express/optional/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/optional/core/schemas/builders/list/list.ts +++ b/seed/ts-express/optional/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/optional/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/optional/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/optional/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/optional/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/optional/core/schemas/builders/object/object.ts b/seed/ts-express/optional/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/optional/core/schemas/builders/object/object.ts +++ b/seed/ts-express/optional/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/optional/core/schemas/builders/record/record.ts b/seed/ts-express/optional/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/optional/core/schemas/builders/record/record.ts +++ b/seed/ts-express/optional/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/optional/core/schemas/builders/union/union.ts b/seed/ts-express/optional/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/optional/core/schemas/builders/union/union.ts +++ b/seed/ts-express/optional/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/optional/core/schemas/utils/isPlainObject.ts b/seed/ts-express/optional/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/optional/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/optional/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/package-yml/core/schemas/builders/list/list.ts b/seed/ts-express/package-yml/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/package-yml/core/schemas/builders/list/list.ts +++ b/seed/ts-express/package-yml/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/package-yml/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/package-yml/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/package-yml/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/package-yml/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/package-yml/core/schemas/builders/object/object.ts b/seed/ts-express/package-yml/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/package-yml/core/schemas/builders/object/object.ts +++ b/seed/ts-express/package-yml/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/package-yml/core/schemas/builders/record/record.ts b/seed/ts-express/package-yml/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/package-yml/core/schemas/builders/record/record.ts +++ b/seed/ts-express/package-yml/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/package-yml/core/schemas/builders/union/union.ts b/seed/ts-express/package-yml/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/package-yml/core/schemas/builders/union/union.ts +++ b/seed/ts-express/package-yml/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/package-yml/core/schemas/utils/isPlainObject.ts b/seed/ts-express/package-yml/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/package-yml/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/package-yml/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/pagination-custom/core/schemas/builders/list/list.ts b/seed/ts-express/pagination-custom/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/pagination-custom/core/schemas/builders/list/list.ts +++ b/seed/ts-express/pagination-custom/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/pagination-custom/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/pagination-custom/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/pagination-custom/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/pagination-custom/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/pagination-custom/core/schemas/builders/object/object.ts b/seed/ts-express/pagination-custom/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/pagination-custom/core/schemas/builders/object/object.ts +++ b/seed/ts-express/pagination-custom/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/pagination-custom/core/schemas/builders/record/record.ts b/seed/ts-express/pagination-custom/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/pagination-custom/core/schemas/builders/record/record.ts +++ b/seed/ts-express/pagination-custom/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/pagination-custom/core/schemas/builders/union/union.ts b/seed/ts-express/pagination-custom/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/pagination-custom/core/schemas/builders/union/union.ts +++ b/seed/ts-express/pagination-custom/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/pagination-custom/core/schemas/utils/isPlainObject.ts b/seed/ts-express/pagination-custom/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/pagination-custom/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/pagination-custom/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/pagination/core/schemas/builders/list/list.ts b/seed/ts-express/pagination/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/pagination/core/schemas/builders/list/list.ts +++ b/seed/ts-express/pagination/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/pagination/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/pagination/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/pagination/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/pagination/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/pagination/core/schemas/builders/object/object.ts b/seed/ts-express/pagination/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/pagination/core/schemas/builders/object/object.ts +++ b/seed/ts-express/pagination/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/pagination/core/schemas/builders/record/record.ts b/seed/ts-express/pagination/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/pagination/core/schemas/builders/record/record.ts +++ b/seed/ts-express/pagination/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/pagination/core/schemas/builders/union/union.ts b/seed/ts-express/pagination/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/pagination/core/schemas/builders/union/union.ts +++ b/seed/ts-express/pagination/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/pagination/core/schemas/utils/isPlainObject.ts b/seed/ts-express/pagination/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/pagination/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/pagination/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/reserved-keywords/core/schemas/builders/list/list.ts b/seed/ts-express/reserved-keywords/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/reserved-keywords/core/schemas/builders/list/list.ts +++ b/seed/ts-express/reserved-keywords/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/reserved-keywords/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/reserved-keywords/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/reserved-keywords/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/reserved-keywords/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/reserved-keywords/core/schemas/builders/object/object.ts b/seed/ts-express/reserved-keywords/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/reserved-keywords/core/schemas/builders/object/object.ts +++ b/seed/ts-express/reserved-keywords/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/reserved-keywords/core/schemas/builders/record/record.ts b/seed/ts-express/reserved-keywords/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/reserved-keywords/core/schemas/builders/record/record.ts +++ b/seed/ts-express/reserved-keywords/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/reserved-keywords/core/schemas/builders/union/union.ts b/seed/ts-express/reserved-keywords/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/reserved-keywords/core/schemas/builders/union/union.ts +++ b/seed/ts-express/reserved-keywords/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/reserved-keywords/core/schemas/utils/isPlainObject.ts b/seed/ts-express/reserved-keywords/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/reserved-keywords/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/reserved-keywords/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/response-property/core/schemas/builders/list/list.ts b/seed/ts-express/response-property/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/response-property/core/schemas/builders/list/list.ts +++ b/seed/ts-express/response-property/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/response-property/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/response-property/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/response-property/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/response-property/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/response-property/core/schemas/builders/object/object.ts b/seed/ts-express/response-property/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/response-property/core/schemas/builders/object/object.ts +++ b/seed/ts-express/response-property/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/response-property/core/schemas/builders/record/record.ts b/seed/ts-express/response-property/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/response-property/core/schemas/builders/record/record.ts +++ b/seed/ts-express/response-property/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/response-property/core/schemas/builders/union/union.ts b/seed/ts-express/response-property/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/response-property/core/schemas/builders/union/union.ts +++ b/seed/ts-express/response-property/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/response-property/core/schemas/utils/isPlainObject.ts b/seed/ts-express/response-property/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/response-property/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/response-property/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/server-url-templating/core/schemas/builders/list/list.ts b/seed/ts-express/server-url-templating/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/server-url-templating/core/schemas/builders/list/list.ts +++ b/seed/ts-express/server-url-templating/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/server-url-templating/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/server-url-templating/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/server-url-templating/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/server-url-templating/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/server-url-templating/core/schemas/builders/object/object.ts b/seed/ts-express/server-url-templating/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/server-url-templating/core/schemas/builders/object/object.ts +++ b/seed/ts-express/server-url-templating/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/server-url-templating/core/schemas/builders/record/record.ts b/seed/ts-express/server-url-templating/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/server-url-templating/core/schemas/builders/record/record.ts +++ b/seed/ts-express/server-url-templating/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/server-url-templating/core/schemas/builders/union/union.ts b/seed/ts-express/server-url-templating/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/server-url-templating/core/schemas/builders/union/union.ts +++ b/seed/ts-express/server-url-templating/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/server-url-templating/core/schemas/utils/isPlainObject.ts b/seed/ts-express/server-url-templating/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/server-url-templating/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/server-url-templating/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/simple-api/core/schemas/builders/list/list.ts b/seed/ts-express/simple-api/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/simple-api/core/schemas/builders/list/list.ts +++ b/seed/ts-express/simple-api/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/simple-api/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/simple-api/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/simple-api/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/simple-api/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/simple-api/core/schemas/builders/object/object.ts b/seed/ts-express/simple-api/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/simple-api/core/schemas/builders/object/object.ts +++ b/seed/ts-express/simple-api/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/simple-api/core/schemas/builders/record/record.ts b/seed/ts-express/simple-api/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/simple-api/core/schemas/builders/record/record.ts +++ b/seed/ts-express/simple-api/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/simple-api/core/schemas/builders/union/union.ts b/seed/ts-express/simple-api/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/simple-api/core/schemas/builders/union/union.ts +++ b/seed/ts-express/simple-api/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/simple-api/core/schemas/utils/isPlainObject.ts b/seed/ts-express/simple-api/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/simple-api/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/simple-api/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/simple-fhir/core/schemas/builders/list/list.ts b/seed/ts-express/simple-fhir/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/simple-fhir/core/schemas/builders/list/list.ts +++ b/seed/ts-express/simple-fhir/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/simple-fhir/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/simple-fhir/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/simple-fhir/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/simple-fhir/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/simple-fhir/core/schemas/builders/object/object.ts b/seed/ts-express/simple-fhir/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/simple-fhir/core/schemas/builders/object/object.ts +++ b/seed/ts-express/simple-fhir/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/simple-fhir/core/schemas/builders/record/record.ts b/seed/ts-express/simple-fhir/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/simple-fhir/core/schemas/builders/record/record.ts +++ b/seed/ts-express/simple-fhir/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/simple-fhir/core/schemas/builders/union/union.ts b/seed/ts-express/simple-fhir/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/simple-fhir/core/schemas/builders/union/union.ts +++ b/seed/ts-express/simple-fhir/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/simple-fhir/core/schemas/utils/isPlainObject.ts b/seed/ts-express/simple-fhir/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/simple-fhir/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/simple-fhir/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/single-url-environment-default/core/schemas/builders/list/list.ts b/seed/ts-express/single-url-environment-default/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/single-url-environment-default/core/schemas/builders/list/list.ts +++ b/seed/ts-express/single-url-environment-default/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/single-url-environment-default/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/single-url-environment-default/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/single-url-environment-default/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/single-url-environment-default/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/single-url-environment-default/core/schemas/builders/object/object.ts b/seed/ts-express/single-url-environment-default/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/single-url-environment-default/core/schemas/builders/object/object.ts +++ b/seed/ts-express/single-url-environment-default/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/single-url-environment-default/core/schemas/builders/record/record.ts b/seed/ts-express/single-url-environment-default/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/single-url-environment-default/core/schemas/builders/record/record.ts +++ b/seed/ts-express/single-url-environment-default/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/single-url-environment-default/core/schemas/builders/union/union.ts b/seed/ts-express/single-url-environment-default/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/single-url-environment-default/core/schemas/builders/union/union.ts +++ b/seed/ts-express/single-url-environment-default/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/single-url-environment-default/core/schemas/utils/isPlainObject.ts b/seed/ts-express/single-url-environment-default/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/single-url-environment-default/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/single-url-environment-default/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/single-url-environment-no-default/core/schemas/builders/list/list.ts b/seed/ts-express/single-url-environment-no-default/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/single-url-environment-no-default/core/schemas/builders/list/list.ts +++ b/seed/ts-express/single-url-environment-no-default/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/single-url-environment-no-default/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/single-url-environment-no-default/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/single-url-environment-no-default/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/single-url-environment-no-default/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/single-url-environment-no-default/core/schemas/builders/object/object.ts b/seed/ts-express/single-url-environment-no-default/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/single-url-environment-no-default/core/schemas/builders/object/object.ts +++ b/seed/ts-express/single-url-environment-no-default/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/single-url-environment-no-default/core/schemas/builders/record/record.ts b/seed/ts-express/single-url-environment-no-default/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/single-url-environment-no-default/core/schemas/builders/record/record.ts +++ b/seed/ts-express/single-url-environment-no-default/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/single-url-environment-no-default/core/schemas/builders/union/union.ts b/seed/ts-express/single-url-environment-no-default/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/single-url-environment-no-default/core/schemas/builders/union/union.ts +++ b/seed/ts-express/single-url-environment-no-default/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/single-url-environment-no-default/core/schemas/utils/isPlainObject.ts b/seed/ts-express/single-url-environment-no-default/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/single-url-environment-no-default/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/single-url-environment-no-default/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/trace/no-custom-config/core/schemas/builders/list/list.ts b/seed/ts-express/trace/no-custom-config/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/trace/no-custom-config/core/schemas/builders/list/list.ts +++ b/seed/ts-express/trace/no-custom-config/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/trace/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/trace/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/trace/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/trace/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/trace/no-custom-config/core/schemas/builders/object/object.ts b/seed/ts-express/trace/no-custom-config/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/trace/no-custom-config/core/schemas/builders/object/object.ts +++ b/seed/ts-express/trace/no-custom-config/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/trace/no-custom-config/core/schemas/builders/record/record.ts b/seed/ts-express/trace/no-custom-config/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/trace/no-custom-config/core/schemas/builders/record/record.ts +++ b/seed/ts-express/trace/no-custom-config/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/trace/no-custom-config/core/schemas/builders/union/union.ts b/seed/ts-express/trace/no-custom-config/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/trace/no-custom-config/core/schemas/builders/union/union.ts +++ b/seed/ts-express/trace/no-custom-config/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/trace/no-custom-config/core/schemas/utils/isPlainObject.ts b/seed/ts-express/trace/no-custom-config/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/trace/no-custom-config/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/trace/no-custom-config/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/list/list.ts b/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/list/list.ts +++ b/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/object/object.ts b/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/object/object.ts +++ b/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/record/record.ts b/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/record/record.ts +++ b/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/union/union.ts b/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/union/union.ts +++ b/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/utils/isPlainObject.ts b/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/ts-express-casing/no-custom-config/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/ts-extra-properties/core/schemas/builders/list/list.ts b/seed/ts-express/ts-extra-properties/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/ts-extra-properties/core/schemas/builders/list/list.ts +++ b/seed/ts-express/ts-extra-properties/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/ts-extra-properties/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/ts-extra-properties/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/ts-extra-properties/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/ts-extra-properties/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/ts-extra-properties/core/schemas/builders/object/object.ts b/seed/ts-express/ts-extra-properties/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/ts-extra-properties/core/schemas/builders/object/object.ts +++ b/seed/ts-express/ts-extra-properties/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/ts-extra-properties/core/schemas/builders/record/record.ts b/seed/ts-express/ts-extra-properties/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/ts-extra-properties/core/schemas/builders/record/record.ts +++ b/seed/ts-express/ts-extra-properties/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/ts-extra-properties/core/schemas/builders/union/union.ts b/seed/ts-express/ts-extra-properties/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/ts-extra-properties/core/schemas/builders/union/union.ts +++ b/seed/ts-express/ts-extra-properties/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/ts-extra-properties/core/schemas/utils/isPlainObject.ts b/seed/ts-express/ts-extra-properties/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/ts-extra-properties/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/ts-extra-properties/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/ts-inline-types/core/schemas/builders/list/list.ts b/seed/ts-express/ts-inline-types/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/ts-inline-types/core/schemas/builders/list/list.ts +++ b/seed/ts-express/ts-inline-types/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/ts-inline-types/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/ts-inline-types/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/ts-inline-types/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/ts-inline-types/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/ts-inline-types/core/schemas/builders/object/object.ts b/seed/ts-express/ts-inline-types/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/ts-inline-types/core/schemas/builders/object/object.ts +++ b/seed/ts-express/ts-inline-types/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/ts-inline-types/core/schemas/builders/record/record.ts b/seed/ts-express/ts-inline-types/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/ts-inline-types/core/schemas/builders/record/record.ts +++ b/seed/ts-express/ts-inline-types/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/ts-inline-types/core/schemas/builders/union/union.ts b/seed/ts-express/ts-inline-types/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/ts-inline-types/core/schemas/builders/union/union.ts +++ b/seed/ts-express/ts-inline-types/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/ts-inline-types/core/schemas/utils/isPlainObject.ts b/seed/ts-express/ts-inline-types/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/ts-inline-types/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/ts-inline-types/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/list/list.ts b/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/list/list.ts +++ b/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/object/object.ts b/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/object/object.ts +++ b/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/record/record.ts b/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/record/record.ts +++ b/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/union/union.ts b/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/union/union.ts +++ b/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/utils/isPlainObject.ts b/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/undiscriminated-union-with-response-property/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/undiscriminated-unions/core/schemas/builders/list/list.ts b/seed/ts-express/undiscriminated-unions/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/undiscriminated-unions/core/schemas/builders/list/list.ts +++ b/seed/ts-express/undiscriminated-unions/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/undiscriminated-unions/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/undiscriminated-unions/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/undiscriminated-unions/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/undiscriminated-unions/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/undiscriminated-unions/core/schemas/builders/object/object.ts b/seed/ts-express/undiscriminated-unions/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/undiscriminated-unions/core/schemas/builders/object/object.ts +++ b/seed/ts-express/undiscriminated-unions/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/undiscriminated-unions/core/schemas/builders/record/record.ts b/seed/ts-express/undiscriminated-unions/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/undiscriminated-unions/core/schemas/builders/record/record.ts +++ b/seed/ts-express/undiscriminated-unions/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/undiscriminated-unions/core/schemas/builders/union/union.ts b/seed/ts-express/undiscriminated-unions/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/undiscriminated-unions/core/schemas/builders/union/union.ts +++ b/seed/ts-express/undiscriminated-unions/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/undiscriminated-unions/core/schemas/utils/isPlainObject.ts b/seed/ts-express/undiscriminated-unions/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/undiscriminated-unions/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/undiscriminated-unions/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/unions-with-local-date/core/schemas/builders/list/list.ts b/seed/ts-express/unions-with-local-date/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/unions-with-local-date/core/schemas/builders/list/list.ts +++ b/seed/ts-express/unions-with-local-date/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/unions-with-local-date/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/unions-with-local-date/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/unions-with-local-date/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/unions-with-local-date/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/unions-with-local-date/core/schemas/builders/object/object.ts b/seed/ts-express/unions-with-local-date/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/unions-with-local-date/core/schemas/builders/object/object.ts +++ b/seed/ts-express/unions-with-local-date/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/unions-with-local-date/core/schemas/builders/record/record.ts b/seed/ts-express/unions-with-local-date/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/unions-with-local-date/core/schemas/builders/record/record.ts +++ b/seed/ts-express/unions-with-local-date/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/unions-with-local-date/core/schemas/builders/union/union.ts b/seed/ts-express/unions-with-local-date/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/unions-with-local-date/core/schemas/builders/union/union.ts +++ b/seed/ts-express/unions-with-local-date/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/unions-with-local-date/core/schemas/utils/isPlainObject.ts b/seed/ts-express/unions-with-local-date/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/unions-with-local-date/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/unions-with-local-date/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/unions/core/schemas/builders/list/list.ts b/seed/ts-express/unions/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/unions/core/schemas/builders/list/list.ts +++ b/seed/ts-express/unions/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/unions/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/unions/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/unions/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/unions/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/unions/core/schemas/builders/object/object.ts b/seed/ts-express/unions/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/unions/core/schemas/builders/object/object.ts +++ b/seed/ts-express/unions/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/unions/core/schemas/builders/record/record.ts b/seed/ts-express/unions/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/unions/core/schemas/builders/record/record.ts +++ b/seed/ts-express/unions/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/unions/core/schemas/builders/union/union.ts b/seed/ts-express/unions/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/unions/core/schemas/builders/union/union.ts +++ b/seed/ts-express/unions/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/unions/core/schemas/utils/isPlainObject.ts b/seed/ts-express/unions/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/unions/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/unions/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/unknown/core/schemas/builders/list/list.ts b/seed/ts-express/unknown/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/unknown/core/schemas/builders/list/list.ts +++ b/seed/ts-express/unknown/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/unknown/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/unknown/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/unknown/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/unknown/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/unknown/core/schemas/builders/object/object.ts b/seed/ts-express/unknown/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/unknown/core/schemas/builders/object/object.ts +++ b/seed/ts-express/unknown/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/unknown/core/schemas/builders/record/record.ts b/seed/ts-express/unknown/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/unknown/core/schemas/builders/record/record.ts +++ b/seed/ts-express/unknown/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/unknown/core/schemas/builders/union/union.ts b/seed/ts-express/unknown/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/unknown/core/schemas/builders/union/union.ts +++ b/seed/ts-express/unknown/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/unknown/core/schemas/utils/isPlainObject.ts b/seed/ts-express/unknown/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/unknown/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/unknown/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/url-form-encoded/core/schemas/builders/list/list.ts b/seed/ts-express/url-form-encoded/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/url-form-encoded/core/schemas/builders/list/list.ts +++ b/seed/ts-express/url-form-encoded/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/url-form-encoded/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/url-form-encoded/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/url-form-encoded/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/url-form-encoded/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/url-form-encoded/core/schemas/builders/object/object.ts b/seed/ts-express/url-form-encoded/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/url-form-encoded/core/schemas/builders/object/object.ts +++ b/seed/ts-express/url-form-encoded/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/url-form-encoded/core/schemas/builders/record/record.ts b/seed/ts-express/url-form-encoded/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/url-form-encoded/core/schemas/builders/record/record.ts +++ b/seed/ts-express/url-form-encoded/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/url-form-encoded/core/schemas/builders/union/union.ts b/seed/ts-express/url-form-encoded/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/url-form-encoded/core/schemas/builders/union/union.ts +++ b/seed/ts-express/url-form-encoded/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/url-form-encoded/core/schemas/utils/isPlainObject.ts b/seed/ts-express/url-form-encoded/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/url-form-encoded/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/url-form-encoded/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/validation/core/schemas/builders/list/list.ts b/seed/ts-express/validation/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/validation/core/schemas/builders/list/list.ts +++ b/seed/ts-express/validation/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/validation/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/validation/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/validation/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/validation/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/validation/core/schemas/builders/object/object.ts b/seed/ts-express/validation/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/validation/core/schemas/builders/object/object.ts +++ b/seed/ts-express/validation/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/validation/core/schemas/builders/record/record.ts b/seed/ts-express/validation/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/validation/core/schemas/builders/record/record.ts +++ b/seed/ts-express/validation/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/validation/core/schemas/builders/union/union.ts b/seed/ts-express/validation/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/validation/core/schemas/builders/union/union.ts +++ b/seed/ts-express/validation/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/validation/core/schemas/utils/isPlainObject.ts b/seed/ts-express/validation/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/validation/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/validation/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/version-no-default/core/schemas/builders/list/list.ts b/seed/ts-express/version-no-default/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/version-no-default/core/schemas/builders/list/list.ts +++ b/seed/ts-express/version-no-default/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/version-no-default/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/version-no-default/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/version-no-default/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/version-no-default/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/version-no-default/core/schemas/builders/object/object.ts b/seed/ts-express/version-no-default/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/version-no-default/core/schemas/builders/object/object.ts +++ b/seed/ts-express/version-no-default/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/version-no-default/core/schemas/builders/record/record.ts b/seed/ts-express/version-no-default/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/version-no-default/core/schemas/builders/record/record.ts +++ b/seed/ts-express/version-no-default/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/version-no-default/core/schemas/builders/union/union.ts b/seed/ts-express/version-no-default/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/version-no-default/core/schemas/builders/union/union.ts +++ b/seed/ts-express/version-no-default/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/version-no-default/core/schemas/utils/isPlainObject.ts b/seed/ts-express/version-no-default/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/version-no-default/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/version-no-default/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/version/core/schemas/builders/list/list.ts b/seed/ts-express/version/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/version/core/schemas/builders/list/list.ts +++ b/seed/ts-express/version/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/version/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/version/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/version/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/version/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/version/core/schemas/builders/object/object.ts b/seed/ts-express/version/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/version/core/schemas/builders/object/object.ts +++ b/seed/ts-express/version/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/version/core/schemas/builders/record/record.ts b/seed/ts-express/version/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/version/core/schemas/builders/record/record.ts +++ b/seed/ts-express/version/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/version/core/schemas/builders/union/union.ts b/seed/ts-express/version/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/version/core/schemas/builders/union/union.ts +++ b/seed/ts-express/version/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/version/core/schemas/utils/isPlainObject.ts b/seed/ts-express/version/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/version/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/version/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/webhook-audience/core/schemas/builders/list/list.ts b/seed/ts-express/webhook-audience/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/webhook-audience/core/schemas/builders/list/list.ts +++ b/seed/ts-express/webhook-audience/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/webhook-audience/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/webhook-audience/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/webhook-audience/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/webhook-audience/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/webhook-audience/core/schemas/builders/object/object.ts b/seed/ts-express/webhook-audience/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/webhook-audience/core/schemas/builders/object/object.ts +++ b/seed/ts-express/webhook-audience/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/webhook-audience/core/schemas/builders/record/record.ts b/seed/ts-express/webhook-audience/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/webhook-audience/core/schemas/builders/record/record.ts +++ b/seed/ts-express/webhook-audience/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/webhook-audience/core/schemas/builders/union/union.ts b/seed/ts-express/webhook-audience/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/webhook-audience/core/schemas/builders/union/union.ts +++ b/seed/ts-express/webhook-audience/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/webhook-audience/core/schemas/utils/isPlainObject.ts b/seed/ts-express/webhook-audience/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/webhook-audience/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/webhook-audience/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/websocket-bearer-auth/core/schemas/builders/list/list.ts b/seed/ts-express/websocket-bearer-auth/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/websocket-bearer-auth/core/schemas/builders/list/list.ts +++ b/seed/ts-express/websocket-bearer-auth/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/websocket-bearer-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/websocket-bearer-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/websocket-bearer-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/websocket-bearer-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/websocket-bearer-auth/core/schemas/builders/object/object.ts b/seed/ts-express/websocket-bearer-auth/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/websocket-bearer-auth/core/schemas/builders/object/object.ts +++ b/seed/ts-express/websocket-bearer-auth/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/websocket-bearer-auth/core/schemas/builders/record/record.ts b/seed/ts-express/websocket-bearer-auth/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/websocket-bearer-auth/core/schemas/builders/record/record.ts +++ b/seed/ts-express/websocket-bearer-auth/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/websocket-bearer-auth/core/schemas/builders/union/union.ts b/seed/ts-express/websocket-bearer-auth/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/websocket-bearer-auth/core/schemas/builders/union/union.ts +++ b/seed/ts-express/websocket-bearer-auth/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/websocket-bearer-auth/core/schemas/utils/isPlainObject.ts b/seed/ts-express/websocket-bearer-auth/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/websocket-bearer-auth/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/websocket-bearer-auth/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/websocket-inferred-auth/core/schemas/builders/list/list.ts b/seed/ts-express/websocket-inferred-auth/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/websocket-inferred-auth/core/schemas/builders/list/list.ts +++ b/seed/ts-express/websocket-inferred-auth/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/websocket-inferred-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/websocket-inferred-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/websocket-inferred-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/websocket-inferred-auth/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/websocket-inferred-auth/core/schemas/builders/object/object.ts b/seed/ts-express/websocket-inferred-auth/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/websocket-inferred-auth/core/schemas/builders/object/object.ts +++ b/seed/ts-express/websocket-inferred-auth/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/websocket-inferred-auth/core/schemas/builders/record/record.ts b/seed/ts-express/websocket-inferred-auth/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/websocket-inferred-auth/core/schemas/builders/record/record.ts +++ b/seed/ts-express/websocket-inferred-auth/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/websocket-inferred-auth/core/schemas/builders/union/union.ts b/seed/ts-express/websocket-inferred-auth/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/websocket-inferred-auth/core/schemas/builders/union/union.ts +++ b/seed/ts-express/websocket-inferred-auth/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/websocket-inferred-auth/core/schemas/utils/isPlainObject.ts b/seed/ts-express/websocket-inferred-auth/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/websocket-inferred-auth/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/websocket-inferred-auth/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-express/websocket/core/schemas/builders/list/list.ts b/seed/ts-express/websocket/core/schemas/builders/list/list.ts index 7c8fd6e87db9..191922f17453 100644 --- a/seed/ts-express/websocket/core/schemas/builders/list/list.ts +++ b/seed/ts-express/websocket/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/websocket/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-express/websocket/core/schemas/builders/object-like/getObjectLikeUtils.ts index ed96cf771cbb..9f2777f6f2d9 100644 --- a/seed/ts-express/websocket/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-express/websocket/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject"; import { getSchemaUtils } from "../schema-utils/index"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-express/websocket/core/schemas/builders/object/object.ts b/seed/ts-express/websocket/core/schemas/builders/object/object.ts index 024a96e54a56..eb48a1116e66 100644 --- a/seed/ts-express/websocket/core/schemas/builders/object/object.ts +++ b/seed/ts-express/websocket/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-express/websocket/core/schemas/builders/record/record.ts b/seed/ts-express/websocket/core/schemas/builders/record/record.ts index ba5307a6a45c..f11dfae0ec67 100644 --- a/seed/ts-express/websocket/core/schemas/builders/record/record.ts +++ b/seed/ts-express/websocket/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema"; -import { entries } from "../../utils/entries"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { isPlainObject } from "../../utils/isPlainObject"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils/index"; import type { BaseRecordSchema, RecordSchema } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-express/websocket/core/schemas/builders/union/union.ts b/seed/ts-express/websocket/core/schemas/builders/union/union.ts index 7da4271a27c0..b324fa2de27e 100644 --- a/seed/ts-express/websocket/core/schemas/builders/union/union.ts +++ b/seed/ts-express/websocket/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-express/websocket/core/schemas/utils/isPlainObject.ts b/seed/ts-express/websocket/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-express/websocket/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-express/websocket/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/list/list.ts b/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/list/list.ts +++ b/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/object/object.ts b/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/object/object.ts +++ b/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/record/record.ts b/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/record/record.ts +++ b/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/union/union.ts b/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/union/union.ts +++ b/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/utils/isPlainObject.ts b/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-sdk/enum/forward-compatible-enums-with-serde/src/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-sdk/enum/serde/src/core/schemas/builders/list/list.ts b/seed/ts-sdk/enum/serde/src/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/seed/ts-sdk/enum/serde/src/core/schemas/builders/list/list.ts +++ b/seed/ts-sdk/enum/serde/src/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/enum/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-sdk/enum/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/seed/ts-sdk/enum/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-sdk/enum/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-sdk/enum/serde/src/core/schemas/builders/object/object.ts b/seed/ts-sdk/enum/serde/src/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/seed/ts-sdk/enum/serde/src/core/schemas/builders/object/object.ts +++ b/seed/ts-sdk/enum/serde/src/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-sdk/enum/serde/src/core/schemas/builders/record/record.ts b/seed/ts-sdk/enum/serde/src/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/seed/ts-sdk/enum/serde/src/core/schemas/builders/record/record.ts +++ b/seed/ts-sdk/enum/serde/src/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/enum/serde/src/core/schemas/builders/union/union.ts b/seed/ts-sdk/enum/serde/src/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/seed/ts-sdk/enum/serde/src/core/schemas/builders/union/union.ts +++ b/seed/ts-sdk/enum/serde/src/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-sdk/enum/serde/src/core/schemas/utils/isPlainObject.ts b/seed/ts-sdk/enum/serde/src/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-sdk/enum/serde/src/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-sdk/enum/serde/src/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/list/list.ts b/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/list/list.ts +++ b/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/object/object.ts b/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/object/object.ts +++ b/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/record/record.ts b/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/record/record.ts +++ b/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/union/union.ts b/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/union/union.ts +++ b/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/utils/isPlainObject.ts b/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-sdk/exhaustive/bigint-serde-layer/src/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/list/list.ts b/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/list/list.ts +++ b/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/object/object.ts b/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/object/object.ts +++ b/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/record/record.ts b/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/record/record.ts +++ b/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/union/union.ts b/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/union/union.ts +++ b/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/utils/isPlainObject.ts b/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-sdk/exhaustive/serde-layer/src/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/list/list.ts b/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/list/list.ts +++ b/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/object/object.ts b/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/object/object.ts +++ b/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/record/record.ts b/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/record/record.ts +++ b/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/union/union.ts b/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/union/union.ts +++ b/seed/ts-sdk/file-upload/serde/src/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-sdk/file-upload/serde/src/core/schemas/utils/isPlainObject.ts b/seed/ts-sdk/file-upload/serde/src/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-sdk/file-upload/serde/src/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-sdk/file-upload/serde/src/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/list/list.ts b/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/list/list.ts +++ b/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/object/object.ts b/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/object/object.ts +++ b/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/record/record.ts b/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/record/record.ts +++ b/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/union/union.ts b/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/union/union.ts +++ b/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/utils/isPlainObject.ts b/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-sdk/oauth-client-credentials/serde/src/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/list/list.ts b/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/list/list.ts +++ b/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/object/object.ts b/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/object/object.ts +++ b/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/record/record.ts b/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/record/record.ts +++ b/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/union/union.ts b/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/union/union.ts +++ b/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/utils/isPlainObject.ts b/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-sdk/path-parameters/no-inline-path-parameters-serde/src/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/list/list.ts b/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/list/list.ts +++ b/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/object/object.ts b/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/object/object.ts +++ b/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/record/record.ts b/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/record/record.ts +++ b/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/union/union.ts b/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/union/union.ts +++ b/seed/ts-sdk/query-parameters/serde/src/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-sdk/query-parameters/serde/src/core/schemas/utils/isPlainObject.ts b/seed/ts-sdk/query-parameters/serde/src/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-sdk/query-parameters/serde/src/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-sdk/query-parameters/serde/src/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/list/list.ts b/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/list/list.ts +++ b/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/object/object.ts b/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/object/object.ts +++ b/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/record/record.ts b/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/record/record.ts +++ b/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/union/union.ts b/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/union/union.ts +++ b/seed/ts-sdk/streaming/serde-layer/src/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-sdk/streaming/serde-layer/src/core/schemas/utils/isPlainObject.ts b/seed/ts-sdk/streaming/serde-layer/src/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-sdk/streaming/serde-layer/src/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-sdk/streaming/serde-layer/src/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/list/list.ts b/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/list/list.ts +++ b/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/object/object.ts b/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/object/object.ts +++ b/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/record/record.ts b/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/record/record.ts +++ b/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/union/union.ts b/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/union/union.ts +++ b/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/utils/isPlainObject.ts b/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-sdk/trace/serde-no-throwing/src/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-sdk/trace/serde/src/core/schemas/builders/list/list.ts b/seed/ts-sdk/trace/serde/src/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/seed/ts-sdk/trace/serde/src/core/schemas/builders/list/list.ts +++ b/seed/ts-sdk/trace/serde/src/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/trace/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-sdk/trace/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/seed/ts-sdk/trace/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-sdk/trace/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-sdk/trace/serde/src/core/schemas/builders/object/object.ts b/seed/ts-sdk/trace/serde/src/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/seed/ts-sdk/trace/serde/src/core/schemas/builders/object/object.ts +++ b/seed/ts-sdk/trace/serde/src/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-sdk/trace/serde/src/core/schemas/builders/record/record.ts b/seed/ts-sdk/trace/serde/src/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/seed/ts-sdk/trace/serde/src/core/schemas/builders/record/record.ts +++ b/seed/ts-sdk/trace/serde/src/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/trace/serde/src/core/schemas/builders/union/union.ts b/seed/ts-sdk/trace/serde/src/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/seed/ts-sdk/trace/serde/src/core/schemas/builders/union/union.ts +++ b/seed/ts-sdk/trace/serde/src/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-sdk/trace/serde/src/core/schemas/utils/isPlainObject.ts b/seed/ts-sdk/trace/serde/src/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-sdk/trace/serde/src/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-sdk/trace/serde/src/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/list/list.ts b/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/list/list.ts +++ b/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/object/object.ts b/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/object/object.ts +++ b/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/record/record.ts b/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/record/record.ts +++ b/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/union/union.ts b/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/union/union.ts +++ b/seed/ts-sdk/ts-extra-properties/src/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-sdk/ts-extra-properties/src/core/schemas/utils/isPlainObject.ts b/seed/ts-sdk/ts-extra-properties/src/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-sdk/ts-extra-properties/src/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-sdk/ts-extra-properties/src/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; } diff --git a/seed/ts-sdk/websocket/serde/src/core/schemas/builders/list/list.ts b/seed/ts-sdk/websocket/serde/src/core/schemas/builders/list/list.ts index 4f8c10ba483a..b3c0bdfcb699 100644 --- a/seed/ts-sdk/websocket/serde/src/core/schemas/builders/list/list.ts +++ b/seed/ts-sdk/websocket/serde/src/core/schemas/builders/list/list.ts @@ -44,30 +44,20 @@ function validateAndTransformArray( }; } - const maybeValidItems = value.map((item, index) => transformItem(item, index)); + const result: Parsed[] = []; + const errors: ValidationError[] = []; - return maybeValidItems.reduce>( - (acc, item) => { - if (acc.ok && item.ok) { - return { - ok: true, - value: [...acc.value, item.value], - }; - } - - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } - if (!item.ok) { - errors.push(...item.errors); - } + for (let i = 0; i < value.length; i++) { + const item = transformItem(value[i], i); + if (item.ok) { + result.push(item.value); + } else { + errors.push(...item.errors); + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: [] }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/websocket/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts b/seed/ts-sdk/websocket/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts index af69acb01dc1..5fdc8023b161 100644 --- a/seed/ts-sdk/websocket/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts +++ b/seed/ts-sdk/websocket/serde/src/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -5,6 +5,9 @@ import { isPlainObject } from "../../utils/isPlainObject.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { ObjectLikeSchema, ObjectLikeUtils } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { return { withParsedProperties: (properties) => withParsedProperties(schema, properties), @@ -26,15 +29,14 @@ export function withParsedProperties>( - (processed, [key, value]) => { - return { - ...processed, - [key]: typeof value === "function" ? value(parsedObject.value) : value, - }; - }, - {}, - ); + const additionalProperties: Record = {}; + for (const key in properties) { + if (_hasOwn.call(properties, key)) { + const value = properties[key as keyof Properties]; + additionalProperties[key] = + typeof value === "function" ? (value as Function)(parsedObject.value) : value; + } + } return { ok: true, diff --git a/seed/ts-sdk/websocket/serde/src/core/schemas/builders/object/object.ts b/seed/ts-sdk/websocket/serde/src/core/schemas/builders/object/object.ts index 9ec570a15d77..0db7f49aa22d 100644 --- a/seed/ts-sdk/websocket/serde/src/core/schemas/builders/object/object.ts +++ b/seed/ts-sdk/websocket/serde/src/core/schemas/builders/object/object.ts @@ -19,6 +19,9 @@ import type { PropertySchemas, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + interface ObjectPropertyWithRawKey { rawKey: string; parsedKey: string; @@ -28,79 +31,128 @@ interface ObjectPropertyWithRawKey { export function object>( schemas: T, ): inferObjectSchemaFromPropertySchemas { - const baseSchema: BaseObjectSchema< - inferRawObjectFromPropertySchemas, - inferParsedObjectFromPropertySchemas - > = { - _getRawProperties: () => - Object.entries(schemas).map(([parsedKey, propertySchema]) => - isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, - ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], - _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + // All property metadata is lazily computed on first use. + // This keeps schema construction free of iteration, which matters when + // many schemas are defined but only some are exercised at runtime. + // Required-key computation is also deferred because lazy() schemas may + // not be resolved at construction time. - parse: (raw, opts) => { - const rawKeyToProperty: Record = {}; - const requiredKeys: string[] = []; + let _rawKeyToProperty: Record | undefined; + function getRawKeyToProperty(): Record { + if (_rawKeyToProperty == null) { + _rawKeyToProperty = {}; for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); const valueSchema: Schema = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.valueSchema : schemaOrObjectProperty; - const property: ObjectPropertyWithRawKey = { + _rawKeyToProperty[rawKey] = { rawKey, parsedKey: parsedKey as string, valueSchema, }; + } + } + return _rawKeyToProperty; + } - rawKeyToProperty[rawKey] = property; + let _parseRequiredKeys: string[] | undefined; + let _jsonRequiredKeys: string[] | undefined; + let _parseRequiredKeysSet: Set | undefined; + let _jsonRequiredKeysSet: Set | undefined; + function getParseRequiredKeys(): string[] { + if (_parseRequiredKeys == null) { + _parseRequiredKeys = []; + _jsonRequiredKeys = []; + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.rawKey + : (parsedKey as string); + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; if (isSchemaRequired(valueSchema)) { - requiredKeys.push(rawKey); + _parseRequiredKeys.push(rawKey); + _jsonRequiredKeys.push(parsedKey as string); } } + _parseRequiredKeysSet = new Set(_parseRequiredKeys); + _jsonRequiredKeysSet = new Set(_jsonRequiredKeys); + } + return _parseRequiredKeys; + } + + function getJsonRequiredKeys(): string[] { + if (_jsonRequiredKeys == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeys!; + } + function getParseRequiredKeysSet(): Set { + if (_parseRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _parseRequiredKeysSet!; + } + + function getJsonRequiredKeysSet(): Set { + if (_jsonRequiredKeysSet == null) { + getParseRequiredKeys(); + } + return _jsonRequiredKeysSet!; + } + + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey, + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: raw, - requiredKeys, + requiredKeys: getParseRequiredKeys(), + requiredKeysSet: getParseRequiredKeysSet(), getProperty: (rawKey) => { - const property = rawKeyToProperty[rawKey]; + const property = getRawKeyToProperty()[rawKey]; if (property == null) { return undefined; } return { transformedKey: property.parsedKey, - transform: (propertyValue) => - property.valueSchema.parse(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, rawKey]; + return property.valueSchema.parse(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, json: (parsed, opts) => { - const requiredKeys: string[] = []; - - for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { - const valueSchema: Schema = isProperty(schemaOrObjectProperty) - ? schemaOrObjectProperty.valueSchema - : schemaOrObjectProperty; - - if (isSchemaRequired(valueSchema)) { - requiredKeys.push(parsedKey as string); - } - } - + const breadcrumbsPrefix = opts?.breadcrumbsPrefix ?? []; return validateAndTransformObject({ value: parsed, - requiredKeys, + requiredKeys: getJsonRequiredKeys(), + requiredKeysSet: getJsonRequiredKeysSet(), getProperty: ( parsedKey, ): { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined => { @@ -114,26 +166,30 @@ export function object - property.valueSchema.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.valueSchema.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } else { return { transformedKey: parsedKey, - transform: (propertyValue) => - property.json(propertyValue, { + transform: (propertyValue) => { + const childBreadcrumbs = [...breadcrumbsPrefix, parsedKey]; + return property.json(propertyValue, { ...opts, - breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], - }), + breadcrumbsPrefix: childBreadcrumbs, + }); + }, }; } }, unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, skipValidation: opts?.skipValidation, - breadcrumbsPrefix: opts?.breadcrumbsPrefix, + breadcrumbsPrefix, omitUndefined: opts?.omitUndefined, }); }, @@ -152,6 +208,7 @@ export function object({ value, requiredKeys, + requiredKeysSet, getProperty, unrecognizedObjectKeys = "fail", skipValidation = false, @@ -159,6 +216,7 @@ function validateAndTransformObject({ }: { value: unknown; requiredKeys: string[]; + requiredKeysSet: Set; getProperty: ( preTransformedKey: string, ) => { transformedKey: string; transform: (propertyValue: object) => MaybeValid } | undefined; @@ -179,15 +237,23 @@ function validateAndTransformObject({ }; } - const missingRequiredKeys = new Set(requiredKeys); + // Track which required keys have been seen. + // Use a counter instead of copying the Set to avoid per-call allocation. + let missingRequiredCount = requiredKeys.length; const errors: ValidationError[] = []; const transformed: Record = {}; - for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + for (const preTransformedKey in value) { + if (!_hasOwn.call(value, preTransformedKey)) { + continue; + } + const preTransformedItemValue = value[preTransformedKey]; const property = getProperty(preTransformedKey); if (property != null) { - missingRequiredKeys.delete(preTransformedKey); + if (missingRequiredCount > 0 && requiredKeysSet.has(preTransformedKey)) { + missingRequiredCount--; + } const value = property.transform(preTransformedItemValue as object); if (value.ok) { @@ -213,14 +279,16 @@ function validateAndTransformObject({ } } - errors.push( - ...requiredKeys - .filter((key) => missingRequiredKeys.has(key)) - .map((key) => ({ - path: breadcrumbsPrefix, - message: `Missing required key "${key}"`, - })), - ); + if (missingRequiredCount > 0) { + for (const key of requiredKeys) { + if (!(key in (value as Record))) { + errors.push({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + }); + } + } + } if (errors.length === 0 || skipValidation) { return { diff --git a/seed/ts-sdk/websocket/serde/src/core/schemas/builders/record/record.ts b/seed/ts-sdk/websocket/serde/src/core/schemas/builders/record/record.ts index a489660399b7..f107fbab7b8e 100644 --- a/seed/ts-sdk/websocket/serde/src/core/schemas/builders/record/record.ts +++ b/seed/ts-sdk/websocket/serde/src/core/schemas/builders/record/record.ts @@ -1,11 +1,13 @@ import { type MaybeValid, type Schema, SchemaType, type ValidationError } from "../../Schema.js"; -import { entries } from "../../utils/entries.js"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType.js"; import { isPlainObject } from "../../utils/isPlainObject.js"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation.js"; import { getSchemaUtils } from "../schema-utils/index.js"; import type { BaseRecordSchema, RecordSchema } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function record( keySchema: Schema, valueSchema: Schema, @@ -79,51 +81,42 @@ function validateAndTransformRecord>>( - (accPromise, [stringKey, value]) => { - if (value === undefined) { - return accPromise; - } - - const acc = accPromise; - - let key: string | number = stringKey; - if (isKeyNumeric) { - const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; - if (!Number.isNaN(numberKey)) { - key = numberKey; - } - } - const transformedKey = transformKey(key); + const result = {} as Record; + const errors: ValidationError[] = []; - const transformedValue = transformValue(value, key); + for (const stringKey in value) { + if (!_hasOwn.call(value, stringKey)) { + continue; + } + const entryValue = (value as Record)[stringKey]; + if (entryValue === undefined) { + continue; + } - if (acc.ok && transformedKey.ok && transformedValue.ok) { - return { - ok: true, - value: { - ...acc.value, - [transformedKey.value]: transformedValue.value, - }, - }; + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!Number.isNaN(numberKey)) { + key = numberKey; } + } + const transformedKey = transformKey(key); + const transformedValue = transformValue(entryValue, key); - const errors: ValidationError[] = []; - if (!acc.ok) { - errors.push(...acc.errors); - } + if (transformedKey.ok && transformedValue.ok) { + result[transformedKey.value] = transformedValue.value; + } else { if (!transformedKey.ok) { errors.push(...transformedKey.errors); } if (!transformedValue.ok) { errors.push(...transformedValue.errors); } + } + } - return { - ok: false, - errors, - }; - }, - { ok: true, value: {} as Record }, - ); + if (errors.length === 0) { + return { ok: true, value: result }; + } + return { ok: false, errors }; } diff --git a/seed/ts-sdk/websocket/serde/src/core/schemas/builders/union/union.ts b/seed/ts-sdk/websocket/serde/src/core/schemas/builders/union/union.ts index 509658e0eb3d..fa993fdc038f 100644 --- a/seed/ts-sdk/websocket/serde/src/core/schemas/builders/union/union.ts +++ b/seed/ts-sdk/websocket/serde/src/core/schemas/builders/union/union.ts @@ -16,6 +16,9 @@ import type { UnionSubtypes, } from "./types.js"; +// eslint-disable-next-line @typescript-eslint/unbound-method +const _hasOwn = Object.prototype.hasOwnProperty; + export function union, U extends UnionSubtypes>( discriminant: D, union: U, @@ -112,7 +115,13 @@ function transformAndValidateUnion< }; } - const { [discriminant]: discriminantValue, ...additionalProperties } = value; + const discriminantValue = value[discriminant]; + const additionalProperties: Record = {}; + for (const key in value) { + if (_hasOwn.call(value, key) && key !== discriminant) { + additionalProperties[key] = value[key]; + } + } if (discriminantValue == null) { return { diff --git a/seed/ts-sdk/websocket/serde/src/core/schemas/utils/isPlainObject.ts b/seed/ts-sdk/websocket/serde/src/core/schemas/utils/isPlainObject.ts index db82a722c35b..32a17e05f01e 100644 --- a/seed/ts-sdk/websocket/serde/src/core/schemas/utils/isPlainObject.ts +++ b/seed/ts-sdk/websocket/serde/src/core/schemas/utils/isPlainObject.ts @@ -4,14 +4,11 @@ export function isPlainObject(value: unknown): value is Record return false; } - if (Object.getPrototypeOf(value) === null) { + const proto = Object.getPrototypeOf(value); + if (proto === null) { return true; } - let proto = value; - while (Object.getPrototypeOf(proto) !== null) { - proto = Object.getPrototypeOf(proto); - } - - return Object.getPrototypeOf(value) === proto; + // Check that the prototype chain has exactly one level (i.e., proto is Object.prototype) + return Object.getPrototypeOf(proto) === null; }