From b908e3f17f455ba5ad62931cfe460c4b45442c80 Mon Sep 17 00:00:00 2001 From: fratzinger <22286818+fratzinger@users.noreply.github.com> Date: Wed, 1 Jul 2026 09:52:02 +0200 Subject: [PATCH 1/2] feat: new util passParams --- package.json | 3 +- pnpm-lock.yaml | 441 +++++++++--------- pnpm-workspace.yaml | 2 + src/hooks/cache/cache.hook.md | 61 ++- src/hooks/cache/cache.hook.test.ts | 97 ++++ src/utils/index.ts | 1 + src/utils/pass-params/pass-params.util.md | 7 + .../pass-params/pass-params.util.test-d.ts | 36 ++ .../pass-params/pass-params.util.test.ts | 132 ++++++ src/utils/pass-params/pass-params.util.ts | 169 +++++++ test/index.test.ts | 1 + 11 files changed, 702 insertions(+), 248 deletions(-) create mode 100644 src/utils/pass-params/pass-params.util.md create mode 100644 src/utils/pass-params/pass-params.util.test-d.ts create mode 100644 src/utils/pass-params/pass-params.util.test.ts create mode 100644 src/utils/pass-params/pass-params.util.ts diff --git a/package.json b/package.json index 34a2cf7..b591adb 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "type": "module", "sideEffects": false, "packageManager": "pnpm@11.5.2", + "prettier": "@feathers-community/eslint-config/prettier", "engines": { "node": ">=22.0.0" }, @@ -97,7 +98,7 @@ "neotraverse": "^0.6.18" }, "devDependencies": { - "@feathers-community/eslint-config": "^0.1.0", + "@feathers-community/eslint-config": "^0.2.0", "@feathersjs/authentication": "^5.0.45", "@feathersjs/authentication-local": "^5.0.45", "@feathersjs/client": "^5.0.45", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6dc9cf2..1fb05ca 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,8 +31,8 @@ importers: version: 0.6.18 devDependencies: '@feathers-community/eslint-config': - specifier: ^0.1.0 - version: 0.1.0(@types/estree@1.0.8)(@typescript-eslint/utils@8.59.2(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.1(jiti@2.6.1))(jsonc-eslint-parser@2.4.1)(prettier@3.8.3)(typescript@5.9.3) + specifier: ^0.2.0 + version: 0.2.0(@types/estree@1.0.8)(@typescript-eslint/utils@8.62.1(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.1(jiti@2.6.1))(prettier@3.8.3)(typescript@5.9.3) '@feathersjs/authentication': specifier: ^5.0.45 version: 5.0.45(typescript@5.9.3) @@ -119,13 +119,13 @@ importers: version: 0.2.17 tsdown: specifier: ^0.22.2 - version: 0.22.2(typescript@5.9.3)(unrun@0.3.1(synckit@0.11.12)) + version: 0.22.2(typescript@5.9.3)(unrun@0.3.1(synckit@0.11.13)) typescript: specifier: ^5.9.3 version: 5.9.3 unrun: specifier: ^0.3.1 - version: 0.3.1(synckit@0.11.12) + version: 0.3.1(synckit@0.11.13) vitepress: specifier: ^2.0.0-alpha.17 version: 2.0.0-alpha.17(@types/node@25.9.2)(change-case@5.4.4)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.15)(sass@1.100.0)(typescript@5.9.3) @@ -166,10 +166,6 @@ packages: resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@8.0.0-rc.4': - resolution: {integrity: sha512-HTD3bskipk5MSm08twTW6832jzIXUhxMddy4NPPzIMuyMEsrs0ZgwAaMj5ubB5+6hMlUjDu17vNconEmwsmpYg==} - engines: {node: ^20.19.0 || >=22.12.0} - '@babel/helper-validator-identifier@8.0.0-rc.6': resolution: {integrity: sha512-nVJ+1JcCgntv8d78rRo++o2wuODT0Irknx2BF8Np4Ft2CRgjLqIs4qzSZ8b66yGbBdMWGmZBO9WEZv1hhNiSpg==} engines: {node: ^22.18.0 || >=24.11.0} @@ -431,8 +427,8 @@ packages: resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@feathers-community/eslint-config@0.1.0': - resolution: {integrity: sha512-PJZ0N98O+30FaGVBrcNFj2SSgmloMeWg8mv8q702iQ/qrpdA7ZkHJD8mq7zvVh406uTPEGZ7pvscd445OYabUQ==} + '@feathers-community/eslint-config@0.2.0': + resolution: {integrity: sha512-py2OEv19BoebS/WB5DFycz8fOqQgK3bWeyt8pe6Dho/EVuRLpxyk53HB0djBXZEQE2a9tw1Bu55Fz2JgCTV2Nw==} peerDependencies: eslint: ^10.0.0 @@ -588,9 +584,6 @@ packages: '@oxc-project/types@0.134.0': resolution: {integrity: sha512-T0xuRRKrQFmocH8y+jGfpmSkGcheaJExY9lEihmR1Gm2aH+75B8CzgU2rABRQSzzDxLjZ15Sc0bRVLj5lVeNXQ==} - '@package-json/types@0.0.12': - resolution: {integrity: sha512-uu43FGU34B5VM9mCNjXCwLaGHYjXdNincqKLaraaCW+7S2+SmiBg1Nv8bPnmschrIfZmfKNY9f3fC376MRrObw==} - '@parcel/watcher-android-arm64@2.5.6': resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} engines: {node: '>= 10.0.0'} @@ -679,9 +672,9 @@ packages: resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==} engines: {node: '>= 10.0.0'} - '@pkgr/core@0.2.9': - resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} - engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@pkgr/core@0.3.6': + resolution: {integrity: sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==} + engines: {node: ^14.18.0 || >=16.0.0} '@quansync/fs@1.0.0': resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} @@ -1273,63 +1266,63 @@ packages: '@types/web-bluetooth@0.0.21': resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} - '@typescript-eslint/eslint-plugin@8.59.2': - resolution: {integrity: sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==} + '@typescript-eslint/eslint-plugin@8.62.1': + resolution: {integrity: sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.59.2 + '@typescript-eslint/parser': ^8.62.1 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.59.2': - resolution: {integrity: sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==} + '@typescript-eslint/parser@8.62.1': + resolution: {integrity: sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.59.2': - resolution: {integrity: sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==} + '@typescript-eslint/project-service@8.62.1': + resolution: {integrity: sha512-yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.59.2': - resolution: {integrity: sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==} + '@typescript-eslint/scope-manager@8.62.1': + resolution: {integrity: sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.59.2': - resolution: {integrity: sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==} + '@typescript-eslint/tsconfig-utils@8.62.1': + resolution: {integrity: sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.59.2': - resolution: {integrity: sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==} + '@typescript-eslint/type-utils@8.62.1': + resolution: {integrity: sha512-aXM5xlqXiTxPibXB93cLAURfT3rlizf7uMXISCXy66Isr/9hISJx3yDsKl0L7lKa51b8JpFuNKby0/O0pEm9jg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.59.2': - resolution: {integrity: sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==} + '@typescript-eslint/types@8.62.1': + resolution: {integrity: sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.59.2': - resolution: {integrity: sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==} + '@typescript-eslint/typescript-estree@8.62.1': + resolution: {integrity: sha512-xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.59.2': - resolution: {integrity: sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==} + '@typescript-eslint/utils@8.62.1': + resolution: {integrity: sha512-sHtbPfuKNZCG+ih8SyjjucqRntSVmp8XgL5u6o9mAhiSn8ds5o/M/XdM0abweme2Tln3szOstOrZ9OXitvPh0g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.59.2': - resolution: {integrity: sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==} + '@typescript-eslint/visitor-keys@8.62.1': + resolution: {integrity: sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript/vfs@1.6.4': @@ -1682,8 +1675,9 @@ packages: resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} engines: {node: ^4.5.0 || >= 5.9} - baseline-browser-mapping@2.9.18: - resolution: {integrity: sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==} + baseline-browser-mapping@2.10.40: + resolution: {integrity: sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==} + engines: {node: '>=6.0.0'} hasBin: true bcryptjs@3.0.3: @@ -1708,8 +1702,8 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.28.1: - resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + browserslist@4.28.4: + resolution: {integrity: sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -1736,8 +1730,8 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} - caniuse-lite@1.0.30001766: - resolution: {integrity: sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==} + caniuse-lite@1.0.30001799: + resolution: {integrity: sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==} ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -1766,10 +1760,6 @@ packages: resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} engines: {node: '>=8'} - clean-regexp@1.0.0: - resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} - engines: {node: '>=4'} - combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -1926,8 +1916,8 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.278: - resolution: {integrity: sha512-dQ0tM1svDRQOwxnXxm+twlGTjr9Upvt8UFWAgmLsxEzFQxhbti4VwxmMjsDxVC51Zo84swW7FVCXEV+VAkhuPw==} + electron-to-chromium@1.5.381: + resolution: {integrity: sha512-n9Wa6yB+vDsGuA8AKbl/0z7HbvWqt5jxIdvr1IUicd0ryPrk7/xzwqLv8D9AbbvZ6avVNtXYLTfmgFHkwkyelg==} empathic@2.0.1: resolution: {integrity: sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==} @@ -1995,10 +1985,6 @@ packages: escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - escape-string-regexp@1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} - escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -2032,8 +2018,8 @@ packages: unrs-resolver: optional: true - eslint-import-resolver-typescript@4.4.4: - resolution: {integrity: sha512-1iM2zeBvrYmUNTj2vSC/90JTHDth+dfOfiNKkxApWRsTJYNrc8rOdxxIf5vazX+BiAXTeOT0UvWpGI/7qIWQOw==} + eslint-import-resolver-typescript@4.4.5: + resolution: {integrity: sha512-nbE5XLph6TLtGYcu/U6e6ZVXyKBhbDWK5cLGk76eJ7NdZpwf1P9EFkpt1Z01mNZNrrilsAYWKH6zUkL4reoXbw==} engines: {node: ^16.17.0 || >=18.6.0} peerDependencies: eslint: '*' @@ -2045,8 +2031,19 @@ packages: eslint-plugin-import-x: optional: true - eslint-plugin-import-x@4.16.2: - resolution: {integrity: sha512-rM9K8UBHcWKpzQzStn1YRN2T5NvdeIfSVoKu/lKF41znQXHAUcBbYXe5wd6GNjZjTrP7viQ49n1D83x/2gYgIw==} + eslint-json-compat-utils@0.2.3: + resolution: {integrity: sha512-RbBmDFyu7FqnjE8F0ZxPNzx5UaptdeS9Uu50r7A+D7s/+FCX+ybiyViYEgFUaFIFqSWJgZRTpL5d8Kanxxl2lQ==} + engines: {node: '>=12'} + peerDependencies: + '@eslint/json': '*' + eslint: '*' + jsonc-eslint-parser: ^2.4.0 || ^3.0.0 + peerDependenciesMeta: + '@eslint/json': + optional: true + + eslint-plugin-import-x@4.17.1: + resolution: {integrity: sha512-4cdstYkKCyjumM2Q9NSI03K8D2a9F4Ssz33K2lv2hQa4KmR9jPLwk3uWGtNvclfqBrPGfGuMBwsGMbe6dMRbfg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: '@typescript-eslint/utils': ^8.56.0 @@ -2058,15 +2055,18 @@ packages: eslint-import-resolver-node: optional: true - eslint-plugin-package-json@0.91.2: - resolution: {integrity: sha512-tuPjHVYOjqEJtErmzuYiQY6o877l9Kb7+lfLhR/mAzHuy9CBqhRreIGPsQmVM/dMJ8yQVg92Bz1vBAgugELIXw==} - engines: {node: ^20.19.0 || >=22.12.0} + eslint-plugin-package-json@1.5.0: + resolution: {integrity: sha512-KQkAdn+7I4vn1W4Va+V8M94RhcgZBvgrxMglDqgEhJgQDRUJ9RdtrEiHoe/84oVTIlp0jiPttLYwkExdvHYhZQ==} + engines: {node: ^22.22.2 || >=24.15.0} peerDependencies: - eslint: '>=8.0.0' - jsonc-eslint-parser: '>=2.0.0' + '@eslint/json': '>=1.0.0' + eslint: '>=9.0.0' + peerDependenciesMeta: + '@eslint/json': + optional: true - eslint-plugin-prettier@5.5.5: - resolution: {integrity: sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==} + eslint-plugin-prettier@5.5.6: + resolution: {integrity: sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: '@types/eslint': '>=8.0.0' @@ -2079,11 +2079,11 @@ packages: eslint-config-prettier: optional: true - eslint-plugin-unicorn@64.0.0: - resolution: {integrity: sha512-rNZwalHh8i0UfPlhNwg5BTUO1CMdKNmjqe+TgzOTZnpKoi8VBgsW7u9qCHIdpxEzZ1uwrJrPF0uRb7l//K38gA==} - engines: {node: ^20.10.0 || >=21.0.0} + eslint-plugin-unicorn@69.0.0: + resolution: {integrity: sha512-ZN/KtHr9hQ6AOByANSNJpsDbo/+Nn+EyQ6blK4w+dcmS/xpYkqLLfrUc+NA/wOK6vF5uEUvhn8my5B/3sruB9g==} + engines: {node: '>=22'} peerDependencies: - eslint: '>=9.38.0' + eslint: '>=10.4' eslint-plugin-unused-imports@4.4.1: resolution: {integrity: sha512-oZGYUz1X3sRMGUB+0cZyK2VcvRX5lm/vB56PgNNcU+7ficUCKm66oZWKUubXWnOuPjQ8PvmXtCViXBMONPe7tQ==} @@ -2120,10 +2120,6 @@ packages: resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - espree@9.6.1: - resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} @@ -2298,8 +2294,8 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - globals@17.6.0: - resolution: {integrity: sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==} + globals@17.7.0: + resolution: {integrity: sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==} engines: {node: '>=18'} gopd@1.2.0: @@ -2479,9 +2475,9 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - jsonc-eslint-parser@2.4.1: - resolution: {integrity: sha512-uuPNLJkKN8NXAlZlQ6kmUF9qO+T6Kyd7oV4+/7yy8Jz6+MZNyhPq8EdLpdfnPVzUC8qSf1b4j1azKaGnFsjmsw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + jsonc-eslint-parser@3.1.0: + resolution: {integrity: sha512-75EA7EWZExL/j+MDKQrRbdzcRI2HOkRlmUw8fZJc1ioqFEOvBsq7Rt+A6yCxOt9w/TYNpkt52gC6nm/g5tFIng==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} jsonwebtoken@9.0.3: resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} @@ -2615,10 +2611,6 @@ packages: longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} - lru-cache@11.3.6: - resolution: {integrity: sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==} - engines: {node: 20 || >=22} - lru-cache@11.5.1: resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} engines: {node: 20 || >=22} @@ -2847,8 +2839,9 @@ packages: node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} - node-releases@2.0.27: - resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + node-releases@2.0.50: + resolution: {integrity: sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==} + engines: {node: '>=18'} npm-check-updates@22.2.3: resolution: {integrity: sha512-n8HZ0ywZugYRgEqIm67hbZC5L/9S/gLX+MkGchJB43pP2xib1+u76OpVEJRZ4QP2BksPRi5M1CCStJVfc16+/A==} @@ -3054,12 +3047,8 @@ packages: regex@6.1.0: resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} - regexp-tree@0.1.27: - resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} - hasBin: true - - regjsparser@0.13.0: - resolution: {integrity: sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==} + regjsparser@0.13.2: + resolution: {integrity: sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==} hasBin: true require-from-string@2.0.2: @@ -3147,6 +3136,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + send@0.19.0: resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} @@ -3227,15 +3221,12 @@ packages: resolution: {integrity: sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==} engines: {node: '>=10.2.0'} - sort-object-keys@1.1.3: - resolution: {integrity: sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg==} - sort-object-keys@2.1.0: resolution: {integrity: sha512-SOiEnthkJKPv2L6ec6HMwhUcN0/lppkeYuN1x63PbyPRrgSPIuBJCiYxYyvWRTtjMlOi14vQUCGUJqS6PLVm8g==} - sort-package-json@3.4.0: - resolution: {integrity: sha512-97oFRRMM2/Js4oEA9LJhjyMlde+2ewpZQf53pgue27UkbEXfHJnDzHlUxQ/DWUkzqmp7DFwJp8D+wi/TYeQhpA==} - engines: {node: '>=20'} + sort-package-json@4.0.0: + resolution: {integrity: sha512-6aYOlYI9AWioZ+rzu+4zKLmoFqJP0/fHDxrd7X04yqEibikY+5YVF0EYlyGn4v6X2PJY7yAUWV7oeP+i5rOm/g==} + engines: {node: '>=22'} hasBin: true source-map-js@1.2.1: @@ -3305,8 +3296,8 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - synckit@0.11.12: - resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==} + synckit@0.11.13: + resolution: {integrity: sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==} engines: {node: ^14.18.0 || >=16.0.0} tabbable@6.4.0: @@ -3424,8 +3415,8 @@ packages: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} - typescript-eslint@8.59.2: - resolution: {integrity: sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ==} + typescript-eslint@8.62.1: + resolution: {integrity: sha512-vymnnM5g0AKQDSAyfP12nMIBvgwgA42syg74kkuZ4x1VuTzwQKwc5h9rGxeShCjny5o+zWAb6OEoz7XLgrIkIw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -3696,8 +3687,6 @@ snapshots: '@babel/helper-validator-identifier@7.29.7': {} - '@babel/helper-validator-identifier@8.0.0-rc.4': {} - '@babel/helper-validator-identifier@8.0.0-rc.6': {} '@babel/parser@7.29.0': @@ -3731,7 +3720,7 @@ snapshots: '@babel/types@8.0.0-rc.4': dependencies: '@babel/helper-string-parser': 8.0.0-rc.4 - '@babel/helper-validator-identifier': 8.0.0-rc.4 + '@babel/helper-validator-identifier': 8.0.0-rc.6 '@babel/types@8.0.0-rc.6': dependencies: @@ -3874,28 +3863,28 @@ snapshots: '@eslint/core': 1.2.1 levn: 0.4.1 - '@feathers-community/eslint-config@0.1.0(@types/estree@1.0.8)(@typescript-eslint/utils@8.59.2(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.1(jiti@2.6.1))(jsonc-eslint-parser@2.4.1)(prettier@3.8.3)(typescript@5.9.3)': + '@feathers-community/eslint-config@0.2.0(@types/estree@1.0.8)(@typescript-eslint/utils@8.62.1(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.1(jiti@2.6.1))(prettier@3.8.3)(typescript@5.9.3)': dependencies: '@eslint/js': 10.0.1(eslint@10.4.1(jiti@2.6.1)) - '@typescript-eslint/eslint-plugin': 8.59.2(@typescript-eslint/parser@8.59.2(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.59.2(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.62.1(@typescript-eslint/parser@8.62.1(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.62.1(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3) eslint: 10.4.1(jiti@2.6.1) eslint-config-prettier: 10.1.8(eslint@10.4.1(jiti@2.6.1)) - eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.59.2(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.1(jiti@2.6.1)))(eslint@10.4.1(jiti@2.6.1)) - eslint-plugin-import-x: 4.16.2(@typescript-eslint/utils@8.59.2(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.1(jiti@2.6.1)) - eslint-plugin-package-json: 0.91.2(@types/estree@1.0.8)(eslint@10.4.1(jiti@2.6.1))(jsonc-eslint-parser@2.4.1) - eslint-plugin-prettier: 5.5.5(eslint-config-prettier@10.1.8(eslint@10.4.1(jiti@2.6.1)))(eslint@10.4.1(jiti@2.6.1))(prettier@3.8.3) - eslint-plugin-unicorn: 64.0.0(eslint@10.4.1(jiti@2.6.1)) - eslint-plugin-unused-imports: 4.4.1(@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.1(jiti@2.6.1)) - globals: 17.6.0 - typescript-eslint: 8.59.2(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3) + eslint-import-resolver-typescript: 4.4.5(eslint-plugin-import-x@4.17.1(@typescript-eslint/utils@8.62.1(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.1(jiti@2.6.1)))(eslint@10.4.1(jiti@2.6.1)) + eslint-plugin-import-x: 4.17.1(@typescript-eslint/utils@8.62.1(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.1(jiti@2.6.1)) + eslint-plugin-package-json: 1.5.0(@types/estree@1.0.8)(eslint@10.4.1(jiti@2.6.1)) + eslint-plugin-prettier: 5.5.6(eslint-config-prettier@10.1.8(eslint@10.4.1(jiti@2.6.1)))(eslint@10.4.1(jiti@2.6.1))(prettier@3.8.3) + eslint-plugin-unicorn: 69.0.0(eslint@10.4.1(jiti@2.6.1)) + eslint-plugin-unused-imports: 4.4.1(@typescript-eslint/eslint-plugin@8.62.1(@typescript-eslint/parser@8.62.1(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.1(jiti@2.6.1)) + globals: 17.7.0 + typescript-eslint: 8.62.1(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3) transitivePeerDependencies: + - '@eslint/json' - '@types/eslint' - '@types/estree' - '@typescript-eslint/utils' - eslint-import-resolver-node - eslint-plugin-import - - jsonc-eslint-parser - prettier - supports-color - typescript @@ -4119,8 +4108,6 @@ snapshots: '@oxc-project/types@0.134.0': {} - '@package-json/types@0.0.12': {} - '@parcel/watcher-android-arm64@2.5.6': optional: true @@ -4182,7 +4169,7 @@ snapshots: '@parcel/watcher-win32-x64': 2.5.6 optional: true - '@pkgr/core@0.2.9': {} + '@pkgr/core@0.3.6': {} '@quansync/fs@1.0.0': dependencies: @@ -4658,14 +4645,14 @@ snapshots: '@types/web-bluetooth@0.0.21': {} - '@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.62.1(@typescript-eslint/parser@8.62.1(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.59.2(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.59.2 - '@typescript-eslint/type-utils': 8.59.2(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.59.2(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.59.2 + '@typescript-eslint/parser': 8.62.1(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.62.1 + '@typescript-eslint/type-utils': 8.62.1(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.62.1(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.62.1 eslint: 10.4.1(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 @@ -4674,41 +4661,41 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.59.2(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.62.1(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.59.2 - '@typescript-eslint/types': 8.59.2 - '@typescript-eslint/typescript-estree': 8.59.2(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.59.2 + '@typescript-eslint/scope-manager': 8.62.1 + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/typescript-estree': 8.62.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.62.1 debug: 4.4.3 eslint: 10.4.1(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.59.2(typescript@5.9.3)': + '@typescript-eslint/project-service@8.62.1(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@5.9.3) - '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/tsconfig-utils': 8.62.1(typescript@5.9.3) + '@typescript-eslint/types': 8.62.1 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.59.2': + '@typescript-eslint/scope-manager@8.62.1': dependencies: - '@typescript-eslint/types': 8.59.2 - '@typescript-eslint/visitor-keys': 8.59.2 + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/visitor-keys': 8.62.1 - '@typescript-eslint/tsconfig-utils@8.59.2(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.62.1(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.59.2(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.62.1(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.59.2 - '@typescript-eslint/typescript-estree': 8.59.2(typescript@5.9.3) - '@typescript-eslint/utils': 8.59.2(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/typescript-estree': 8.62.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.62.1(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3 eslint: 10.4.1(jiti@2.6.1) ts-api-utils: 2.5.0(typescript@5.9.3) @@ -4716,37 +4703,37 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.59.2': {} + '@typescript-eslint/types@8.62.1': {} - '@typescript-eslint/typescript-estree@8.59.2(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.62.1(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.59.2(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@5.9.3) - '@typescript-eslint/types': 8.59.2 - '@typescript-eslint/visitor-keys': 8.59.2 + '@typescript-eslint/project-service': 8.62.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.62.1(typescript@5.9.3) + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/visitor-keys': 8.62.1 debug: 4.4.3 minimatch: 10.2.5 - semver: 7.8.0 - tinyglobby: 0.2.16 + semver: 7.8.5 + tinyglobby: 0.2.17 ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.59.2(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.62.1(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.59.2 - '@typescript-eslint/types': 8.59.2 - '@typescript-eslint/typescript-estree': 8.59.2(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.62.1 + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/typescript-estree': 8.62.1(typescript@5.9.3) eslint: 10.4.1(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.59.2': + '@typescript-eslint/visitor-keys@8.62.1': dependencies: - '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/types': 8.62.1 eslint-visitor-keys: 5.0.1 '@typescript/vfs@1.6.4(typescript@5.9.3)': @@ -5101,7 +5088,7 @@ snapshots: base64id@2.0.0: {} - baseline-browser-mapping@2.9.18: {} + baseline-browser-mapping@2.10.40: {} bcryptjs@3.0.3: {} @@ -5134,13 +5121,13 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.28.1: + browserslist@4.28.4: dependencies: - baseline-browser-mapping: 2.9.18 - caniuse-lite: 1.0.30001766 - electron-to-chromium: 1.5.278 - node-releases: 2.0.27 - update-browserslist-db: 1.2.3(browserslist@4.28.1) + baseline-browser-mapping: 2.10.40 + caniuse-lite: 1.0.30001799 + electron-to-chromium: 1.5.381 + node-releases: 2.0.50 + update-browserslist-db: 1.2.3(browserslist@4.28.4) buffer-equal-constant-time@1.0.1: {} @@ -5160,7 +5147,7 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 - caniuse-lite@1.0.30001766: {} + caniuse-lite@1.0.30001799: {} ccount@2.0.1: {} @@ -5180,10 +5167,6 @@ snapshots: ci-info@4.4.0: {} - clean-regexp@1.0.0: - dependencies: - escape-string-regexp: 1.0.5 - combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -5228,7 +5211,7 @@ snapshots: core-js-compat@3.49.0: dependencies: - browserslist: 4.28.1 + browserslist: 4.28.4 cors@2.8.6: dependencies: @@ -5305,7 +5288,7 @@ snapshots: ee-first@1.1.1: {} - electron-to-chromium@1.5.278: {} + electron-to-chromium@1.5.381: {} empathic@2.0.1: {} @@ -5394,8 +5377,6 @@ snapshots: escape-html@1.0.3: {} - escape-string-regexp@1.0.5: {} - escape-string-regexp@4.0.0: {} escape-string-regexp@5.0.0: {} @@ -5417,7 +5398,7 @@ snapshots: optionalDependencies: unrs-resolver: 1.11.1 - eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.59.2(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.1(jiti@2.6.1)))(eslint@10.4.1(jiti@2.6.1)): + eslint-import-resolver-typescript@4.4.5(eslint-plugin-import-x@4.17.1(@typescript-eslint/utils@8.62.1(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.1(jiti@2.6.1)))(eslint@10.4.1(jiti@2.6.1)): dependencies: debug: 4.4.3 eslint: 10.4.1(jiti@2.6.1) @@ -5425,32 +5406,37 @@ snapshots: get-tsconfig: 4.13.6 is-bun-module: 2.0.0 stable-hash-x: 0.2.0 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import-x: 4.16.2(@typescript-eslint/utils@8.59.2(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.1(jiti@2.6.1)) + eslint-plugin-import-x: 4.17.1(@typescript-eslint/utils@8.62.1(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.59.2(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.1(jiti@2.6.1)): + eslint-json-compat-utils@0.2.3(eslint@10.4.1(jiti@2.6.1))(jsonc-eslint-parser@3.1.0): dependencies: - '@package-json/types': 0.0.12 - '@typescript-eslint/types': 8.59.2 + eslint: 10.4.1(jiti@2.6.1) + esquery: 1.7.0 + jsonc-eslint-parser: 3.1.0 + + eslint-plugin-import-x@4.17.1(@typescript-eslint/utils@8.62.1(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.1(jiti@2.6.1)): + dependencies: + '@typescript-eslint/types': 8.62.1 comment-parser: 1.4.1 debug: 4.4.3 eslint: 10.4.1(jiti@2.6.1) eslint-import-context: 0.1.9(unrs-resolver@1.11.1) is-glob: 4.0.3 minimatch: 10.2.5 - semver: 7.8.0 + semver: 7.8.5 stable-hash-x: 0.2.0 unrs-resolver: 1.11.1 optionalDependencies: - '@typescript-eslint/utils': 8.59.2(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.62.1(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3) transitivePeerDependencies: - supports-color - eslint-plugin-package-json@0.91.2(@types/estree@1.0.8)(eslint@10.4.1(jiti@2.6.1))(jsonc-eslint-parser@2.4.1): + eslint-plugin-package-json@1.5.0(@types/estree@1.0.8)(eslint@10.4.1(jiti@2.6.1)): dependencies: '@altano/repository-tools': 2.0.1 change-case: 5.4.4 @@ -5458,49 +5444,49 @@ snapshots: detect-newline: 4.0.1 eslint: 10.4.1(jiti@2.6.1) eslint-fix-utils: 0.4.2(@types/estree@1.0.8)(eslint@10.4.1(jiti@2.6.1)) - jsonc-eslint-parser: 2.4.1 + eslint-json-compat-utils: 0.2.3(eslint@10.4.1(jiti@2.6.1))(jsonc-eslint-parser@3.1.0) + jsonc-eslint-parser: 3.1.0 package-json-validator: 1.5.1 - semver: 7.8.0 + semver: 7.8.5 sort-object-keys: 2.1.0 - sort-package-json: 3.4.0 - validate-npm-package-name: 7.0.2 + sort-package-json: 4.0.0 transitivePeerDependencies: - '@types/estree' - eslint-plugin-prettier@5.5.5(eslint-config-prettier@10.1.8(eslint@10.4.1(jiti@2.6.1)))(eslint@10.4.1(jiti@2.6.1))(prettier@3.8.3): + eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@10.4.1(jiti@2.6.1)))(eslint@10.4.1(jiti@2.6.1))(prettier@3.8.3): dependencies: eslint: 10.4.1(jiti@2.6.1) prettier: 3.8.3 prettier-linter-helpers: 1.0.1 - synckit: 0.11.12 + synckit: 0.11.13 optionalDependencies: eslint-config-prettier: 10.1.8(eslint@10.4.1(jiti@2.6.1)) - eslint-plugin-unicorn@64.0.0(eslint@10.4.1(jiti@2.6.1)): + eslint-plugin-unicorn@69.0.0(eslint@10.4.1(jiti@2.6.1)): dependencies: - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-validator-identifier': 7.29.7 '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.1(jiti@2.6.1)) + browserslist: 4.28.4 change-case: 5.4.4 ci-info: 4.4.0 - clean-regexp: 1.0.0 core-js-compat: 3.49.0 + detect-indent: 7.0.2 eslint: 10.4.1(jiti@2.6.1) find-up-simple: 1.0.1 - globals: 17.6.0 + globals: 17.7.0 indent-string: 5.0.0 is-builtin-module: 5.0.0 jsesc: 3.1.0 pluralize: 8.0.0 - regexp-tree: 0.1.27 - regjsparser: 0.13.0 - semver: 7.8.0 + regjsparser: 0.13.2 + semver: 7.8.5 strip-indent: 4.1.1 - eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.1(jiti@2.6.1)): + eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.62.1(@typescript-eslint/parser@8.62.1(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.1(jiti@2.6.1)): dependencies: eslint: 10.4.1(jiti@2.6.1) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.59.2(@typescript-eslint/parser@8.59.2(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.62.1(@typescript-eslint/parser@8.62.1(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3) eslint-scope@9.1.2: dependencies: @@ -5556,12 +5542,6 @@ snapshots: acorn-jsx: 5.3.2(acorn@8.16.0) eslint-visitor-keys: 5.0.1 - espree@9.6.1: - dependencies: - acorn: 8.16.0 - acorn-jsx: 5.3.2(acorn@8.16.0) - eslint-visitor-keys: 3.4.3 - esprima@4.0.1: {} esquery@1.7.0: @@ -5767,7 +5747,7 @@ snapshots: dependencies: is-glob: 4.0.3 - globals@17.6.0: {} + globals@17.7.0: {} gopd@1.2.0: {} @@ -5816,7 +5796,7 @@ snapshots: hosted-git-info@9.0.3: dependencies: - lru-cache: 11.3.6 + lru-cache: 11.5.1 html-escaper@2.0.2: {} @@ -5858,7 +5838,7 @@ snapshots: is-bun-module@2.0.0: dependencies: - semver: 7.8.0 + semver: 7.8.5 is-core-module@2.16.1: dependencies: @@ -5919,12 +5899,11 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} - jsonc-eslint-parser@2.4.1: + jsonc-eslint-parser@3.1.0: dependencies: acorn: 8.16.0 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 - semver: 7.8.3 + eslint-visitor-keys: 5.0.1 + semver: 7.8.5 jsonwebtoken@9.0.3: dependencies: @@ -6038,8 +6017,6 @@ snapshots: longest-streak@3.1.0: {} - lru-cache@11.3.6: {} - lru-cache@11.5.1: {} lz-string@1.5.0: {} @@ -6380,7 +6357,7 @@ snapshots: node-addon-api@7.1.1: optional: true - node-releases@2.0.27: {} + node-releases@2.0.50: {} npm-check-updates@22.2.3: {} @@ -6388,7 +6365,7 @@ snapshots: dependencies: hosted-git-info: 9.0.3 proc-log: 6.1.0 - semver: 7.8.0 + semver: 7.8.5 validate-npm-package-name: 7.0.2 npm-run-path@2.0.2: @@ -6451,7 +6428,7 @@ snapshots: package-json-validator@1.5.1: dependencies: npm-package-arg: 13.0.2 - semver: 7.8.0 + semver: 7.8.5 validate-npm-package-license: 3.0.4 validate-npm-package-name: 7.0.2 @@ -6562,9 +6539,7 @@ snapshots: dependencies: regex-utilities: 2.3.0 - regexp-tree@0.1.27: {} - - regjsparser@0.13.0: + regjsparser@0.13.2: dependencies: jsesc: 3.1.0 @@ -6695,6 +6670,8 @@ snapshots: semver@7.8.3: {} + semver@7.8.5: {} + send@0.19.0: dependencies: debug: 2.6.9 @@ -6834,19 +6811,17 @@ snapshots: - supports-color - utf-8-validate - sort-object-keys@1.1.3: {} - sort-object-keys@2.1.0: {} - sort-package-json@3.4.0: + sort-package-json@4.0.0: dependencies: detect-indent: 7.0.2 detect-newline: 4.0.1 git-hooks-list: 4.1.1 is-plain-obj: 4.1.0 - semver: 7.8.0 - sort-object-keys: 1.1.3 - tinyglobby: 0.2.16 + semver: 7.8.5 + sort-object-keys: 2.1.0 + tinyglobby: 0.2.17 source-map-js@1.2.1: {} @@ -6899,9 +6874,9 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - synckit@0.11.12: + synckit@0.11.13: dependencies: - '@pkgr/core': 0.2.9 + '@pkgr/core': 0.3.6 tabbable@6.4.0: {} @@ -6943,7 +6918,7 @@ snapshots: dependencies: typescript: 5.9.3 - tsdown@0.22.2(typescript@5.9.3)(unrun@0.3.1(synckit@0.11.12)): + tsdown@0.22.2(typescript@5.9.3)(unrun@0.3.1(synckit@0.11.13)): dependencies: ansis: 4.3.1 cac: 7.0.0 @@ -6962,7 +6937,7 @@ snapshots: unconfig-core: 7.5.0 optionalDependencies: typescript: 5.9.3 - unrun: 0.3.1(synckit@0.11.12) + unrun: 0.3.1(synckit@0.11.13) transitivePeerDependencies: - '@ts-macro/tsc' - '@typescript/native-preview' @@ -7000,12 +6975,12 @@ snapshots: media-typer: 0.3.0 mime-types: 2.1.35 - typescript-eslint@8.59.2(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3): + typescript-eslint@8.62.1(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.59.2(@typescript-eslint/parser@8.59.2(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.59.2(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.59.2(typescript@5.9.3) - '@typescript-eslint/utils': 8.59.2(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.62.1(@typescript-eslint/parser@8.62.1(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.62.1(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.62.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.62.1(eslint@10.4.1(jiti@2.6.1))(typescript@5.9.3) eslint: 10.4.1(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: @@ -7073,15 +7048,15 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 - unrun@0.3.1(synckit@0.11.12): + unrun@0.3.1(synckit@0.11.13): dependencies: rolldown: 1.0.0 optionalDependencies: - synckit: 0.11.12 + synckit: 0.11.13 - update-browserslist-db@1.2.3(browserslist@4.28.1): + update-browserslist-db@1.2.3(browserslist@4.28.4): dependencies: - browserslist: 4.28.1 + browserslist: 4.28.4 escalade: 3.2.0 picocolors: 1.1.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ae1839d..547abdf 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,3 +2,5 @@ allowBuilds: "@parcel/watcher": true esbuild: true unrs-resolver: true +minimumReleaseAgeExclude: + - '@feathers-community/eslint-config@0.2.0' diff --git a/src/hooks/cache/cache.hook.md b/src/hooks/cache/cache.hook.md index 9b0c046..14d13f3 100644 --- a/src/hooks/cache/cache.hook.md +++ b/src/hooks/cache/cache.hook.md @@ -2,8 +2,8 @@ title: cache category: hooks hook: - type: ["before", "after"] - method: ["find", "get", "create", "update", "patch", "remove"] + type: ['before', 'after'] + method: ['find', 'get', 'create', 'update', 'patch', 'remove'] multi: true --- @@ -15,11 +15,11 @@ The `cache` hook caches `get` and `find` results based on `params`. On mutating ## Options -| Option | Type | Description | -| --- | --- | --- | -| `map` | `Cache` | The cache implementation. Must implement `get`, `set`, `delete`, `clear`, and `keys`. | -| `id` | `string` | The id field to use. Defaults to `service.options.id`, then `'id'`. | -| `transformParams` | `(params) => params` | Transform params before they are used as cache key. Use this to exclude properties like `paginate` or `user` from the cache key. | +| Option | Type | Description | +| ----------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `map` | `Cache` | The cache implementation. Must implement `get`, `set`, `delete`, `clear`, and `keys`. | +| `id` | `string` | The id field to use. Defaults to `service.options.id`, then `'id'`. | +| `transformParams` | `(params) => params` | Transform params before they are used as cache key. Compose it with [`passParams`](/utils/pass-params) to declaratively pick/drop keys and avoid false hits — see [Choosing Cache-Relevant Params](#choosing-cache-relevant-params-with-passparams). | ## Cache Interface @@ -115,7 +115,7 @@ const redisCache = { // For production use, consider maintaining a local Set of active keys. throw new Error( 'Synchronous keys iteration is not supported with Redis. ' + - 'Use clear() for full invalidation instead.' + 'Use clear() for full invalidation instead.', ) }, } @@ -212,16 +212,49 @@ app.service('users').hooks({ }) ``` -### Excluding Params from Cache Key +### Choosing Cache-Relevant Params (with `passParams`) -Use `transformParams` to exclude properties that should not affect the cache key, such as `paginate`, `user`, or authentication info: +Deciding which `params` keys form the cache key is the trickiest part of caching, and the two failure modes are asymmetric: + +- **False hits (dangerous):** if a key that affects the result is left out (e.g. `user`/tenant, `provider`), two semantically different requests collapse to the same key — one user can be served another user's cached data. +- **False misses (wasteful):** if a per-request/metrics key is included (e.g. `rateLimit`), every request produces a unique key and the cache never hits. A function-valued key (e.g. `stashed` from `stashable`) would even make serialization throw. + +The [`passParams`](/utils/pass-params) utility makes this explicit and safe. It takes a declarative path schema (`true` include, `false` drop, or a predicate/projection function). `query` is always included by default, and keys you never classified are **kept by default** — the safe direction, since a forgotten key causes at worst a harmless cache miss, never a false hit. + +> Transient keys that feathers-utils' own hooks attach to `params` — `rateLimit` (`rateLimit`), `skipHooks` (`skippable`/`addSkip`), the `stashed` function and `_stashable` flag (`stashable`) — are never cache-relevant. Drop them with `false`, or keep only what you list via `dropUnknownParams: true`. + +#### Exclude specific params (default) + +Cache on everything except the keys you explicitly drop with `false`. This is the default direction — safe against false hits: ```ts +import { passParams } from 'feathers-utils/utils' + cache({ map: new Map(), - transformParams: (params) => { - const { paginate, user, authentication, ...rest } = params as any - return rest - }, + transformParams: (params) => + passParams(params, { rateLimit: false, skipHooks: false }), +}) +``` + +#### Include only specific params + +Set `dropUnknownParams: true` so only `query` (always) and the listed paths form the cache key. `user.id` is picked via dot-notation so different tenants never collide and per-request `user` fields don't bloat the key. Use `onUnknownParams` to log anything that was dropped: + +```ts +import { passParams } from 'feathers-utils/utils' + +cache({ + map: new Map(), + transformParams: (params) => + passParams( + params, + { 'user.id': true }, // `query` is included automatically + { + dropUnknownParams: true, + onUnknownParams: (keys) => + keys.forEach((key) => logger.warn('undeclared cache param', key)), + }, + ), }) ``` diff --git a/src/hooks/cache/cache.hook.test.ts b/src/hooks/cache/cache.hook.test.ts index f089884..f1171c8 100644 --- a/src/hooks/cache/cache.hook.test.ts +++ b/src/hooks/cache/cache.hook.test.ts @@ -7,6 +7,7 @@ import { TTLCache } from '@isaacs/ttlcache' import { MemoryService } from '@feathersjs/memory' import { expect, expectTypeOf } from 'vitest' import { copy } from 'fast-copy' +import { passParams } from '../../utils/pass-params/pass-params.util.js' const setup = (options: CacheOptions, serviceOptions?: { id?: string }) => { const app = feathers<{ @@ -1147,3 +1148,99 @@ describe('cache hook as an around hook', () => { expect(getSpy).toHaveBeenCalledTimes(2) }) }) + +describe('cache hook with passParams', () => { + it('prevents false hits across users and collapses non-id user fields (whitelist)', async () => { + const { usersService, before } = setup({ + map: new Map(), + // `query` is included by default; only `user.id` is added explicitly. + transformParams: (params) => passParams(params, { 'user.id': true }), + }) + + await usersService.create({ id: 1, name: 'John' }) + + // user a -> miss + await usersService.find({ + query: {}, + user: { id: 'a', updatedAt: 1 }, + } as any) + expect(before.find).toHaveBeenCalledTimes(1) + + // same user id, different transient timestamp -> hit (projection collapses it) + await usersService.find({ + query: {}, + user: { id: 'a', updatedAt: 999 }, + } as any) + expect(before.find).toHaveBeenCalledTimes(1) + + // different user id -> miss (no cross-user leak) + await usersService.find({ + query: {}, + user: { id: 'b', updatedAt: 1 }, + } as any) + expect(before.find).toHaveBeenCalledTimes(2) + }) + + it('strips transient metric keys so the cache still hits (blacklist)', async () => { + const { usersService, before } = setup({ + map: new Map(), + // keep everything except the transient `rateLimit` metric. + transformParams: (params) => passParams(params, { rateLimit: false }), + }) + + await usersService.create({ id: 1, name: 'John' }) + + await usersService.find({ + query: { name: 'John' }, + rateLimit: { remainingPoints: 9 }, + } as any) + expect(before.find).toHaveBeenCalledTimes(1) + + // identical query, different rate-limit metrics -> still a cache hit + await usersService.find({ + query: { name: 'John' }, + rateLimit: { remainingPoints: 8 }, + } as any) + expect(before.find).toHaveBeenCalledTimes(1) + }) + + it('drops function-valued params under a whitelist so serialization never throws', async () => { + const { usersService, before } = setup({ + map: new Map(), + // keep only `query` (default); `stashed` (a function) is dropped. + transformParams: (params) => + passParams(params, {}, { dropUnknownParams: true }), + }) + + await usersService.create({ id: 1, name: 'John' }) + + // `stashed` is a function; if it reached stableStringify it would throw. + await expect( + usersService.find({ + query: { name: 'John' }, + stashed: () => Promise.resolve(), + } as any), + ).resolves.toBeDefined() + expect(before.find).toHaveBeenCalledTimes(1) + + await usersService.find({ + query: { name: 'John' }, + stashed: () => Promise.resolve('other'), + } as any) + expect(before.find).toHaveBeenCalledTimes(1) // cache hit + }) + + it('reports undeclared params keys via onUnknownParams', async () => { + const onUnknownParams = vi.fn() + const { usersService } = setup({ + map: new Map(), + transformParams: (params) => + passParams(params, { query: true }, { onUnknownParams }), + }) + + await usersService.create({ id: 1, name: 'John' }) + await usersService.find({ query: {}, mystery: 1 } as any) + + expect(onUnknownParams).toHaveBeenCalledWith(['mystery'], expect.anything()) + }) +}) diff --git a/src/utils/index.ts b/src/utils/index.ts index 738977f..0e35ab8 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -12,6 +12,7 @@ export * from './iterate-find/iterate-find.util.js' export * from './merge-query/merge-query.util.js' export * from './mutate-data/mutate-data.util.js' export * from './mutate-result/mutate-result.util.js' +export * from './pass-params/pass-params.util.js' export * from './patch-batch/patch-batch.util.js' export * from './query-defaults/query-defaults.util.js' export * from './query-has-property/query-has-property.util.js' diff --git a/src/utils/pass-params/pass-params.util.md b/src/utils/pass-params/pass-params.util.md new file mode 100644 index 0000000..b269534 --- /dev/null +++ b/src/utils/pass-params/pass-params.util.md @@ -0,0 +1,7 @@ +--- +title: passParams +category: utils +see: + - hooks/cache + - utils/transform-params +--- diff --git a/src/utils/pass-params/pass-params.util.test-d.ts b/src/utils/pass-params/pass-params.util.test-d.ts new file mode 100644 index 0000000..820dcf7 --- /dev/null +++ b/src/utils/pass-params/pass-params.util.test-d.ts @@ -0,0 +1,36 @@ +import type { Params } from '@feathersjs/feathers' +import { expectTypeOf } from 'vitest' +import { passParams } from './pass-params.util.js' + +it('returns a Params object', () => { + const out = passParams({ query: {} }, { query: true }) + expectTypeOf(out).toEqualTypeOf() +}) + +it('accepts boolean and function rules, including nested paths and custom keys', () => { + passParams({ query: {}, user: { id: 1 }, custom: 1 } as Params, { + query: true, + paginate: false, + 'user.id': true, + user: (value, params) => { + expectTypeOf(value).toBeAny() + expectTypeOf(params).toEqualTypeOf() + return value?.id + }, + custom: true, + }) +}) + +it('types onUnknownParams and dropUnknownParams', () => { + passParams( + { query: {} } as Params, + { query: true }, + { + dropUnknownParams: true, + onUnknownParams: (keys, params) => { + expectTypeOf(keys).toEqualTypeOf() + expectTypeOf(params).toEqualTypeOf() + }, + }, + ) +}) diff --git a/src/utils/pass-params/pass-params.util.test.ts b/src/utils/pass-params/pass-params.util.test.ts new file mode 100644 index 0000000..376628e --- /dev/null +++ b/src/utils/pass-params/pass-params.util.test.ts @@ -0,0 +1,132 @@ +import { describe, it, expect, vi } from 'vitest' +import _omit from 'lodash/omit.js' +import { passParams } from './pass-params.util.js' + +describe('passParams', () => { + it('rule true includes the value as-is', () => { + const params = { user: { id: 5 } } as any + expect(passParams(params, { user: true })).toEqual({ user: { id: 5 } }) + }) + + it('rule false drops the key', () => { + const params = { query: { a: 1 }, paginate: false } as any + expect(passParams(params, { query: true, paginate: false })).toEqual({ + query: { a: 1 }, + }) + }) + + it('function returning true includes as-is', () => { + const params = { query: { a: 1 } } as any + expect(passParams(params, { query: () => true })).toEqual({ + query: { a: 1 }, + }) + }) + + it('function returning false or undefined drops the key', () => { + const params = { a: 1, b: 2 } as any + expect(passParams(params, { a: () => false, b: () => undefined })).toEqual( + {}, + ) + }) + + it('function return value is used as a projection', () => { + const params = { user: { id: 5, name: 'John' } } as any + expect(passParams(params, { user: (u) => u?.id })).toEqual({ user: 5 }) + }) + + it('picks a nested value via dot-notation path', () => { + const params = { user: { id: 5, updatedAt: 123 } } as any + expect(passParams(params, { 'user.id': true })).toEqual({ user: { id: 5 } }) + }) + + it('skips a schema path that is absent from params (no injection)', () => { + const params = { query: {} } as any + expect(passParams(params, { query: true, 'user.id': true })).toEqual({ + query: {}, + }) + }) + + it('does not inject a top-level schema key absent from params', () => { + const params = { query: {} } as any + expect(passParams(params, { query: true, user: true })).toEqual({ + query: {}, + }) + }) + + it('drops a nested key while keeping the parent via a projection', () => { + const params = { query: { userId: 1, _$client: { foo: 1 } } } as any + expect(passParams(params, { query: (q) => _omit(q, '_$client') })).toEqual({ + query: { userId: 1 }, + }) + }) + + it('includes query by default when the schema omits it', () => { + const params = { query: { a: 1 } } as any + expect(passParams(params, {})).toEqual({ query: { a: 1 } }) + }) + + it('does not include query by default when the schema sets it to false', () => { + const params = { query: { a: 1 } } as any + expect(passParams(params, { query: false })).toEqual({}) + }) + + it('does not default the whole query when a nested query path is declared', () => { + const params = { query: { a: 1, b: 2 } } as any + expect(passParams(params, { 'query.a': true })).toEqual({ query: { a: 1 } }) + }) + + it('keeps unknown keys by default', () => { + const params = { query: {}, foo: 1 } as any + expect(passParams(params, {})).toEqual({ query: {}, foo: 1 }) + }) + + it('drops unknown keys with dropUnknownParams: true', () => { + const params = { query: {}, foo: 1 } as any + expect( + passParams(params, { 'user.id': true }, { dropUnknownParams: true }), + ).toEqual({ query: {} }) + }) + + it('keeps unknown keys except the ones the schema drops with false', () => { + const params = { query: {}, foo: 1, rateLimit: { remaining: 9 } } as any + expect(passParams(params, { rateLimit: false })).toEqual({ + query: {}, + foo: 1, + }) + }) + + it('does not report a nested path root as unknown', () => { + const params = { query: {}, user: { id: 5, updatedAt: 1 } } as any + const onUnknownParams = vi.fn() + + const out = passParams( + params, + { query: true, 'user.id': true }, + { onUnknownParams }, + ) + + // `user` is claimed by the `user.id` path, so it must not be reported unknown + expect(onUnknownParams).not.toHaveBeenCalled() + expect(out).toEqual({ query: {}, user: { id: 5 } }) + }) + + it('reports unknown keys once via onUnknownParams, excluding schema-false keys', () => { + const params = { query: {}, rateLimit: { r: 1 }, mystery: 2 } as any + const onUnknownParams = vi.fn() + + passParams(params, { query: true, rateLimit: false }, { onUnknownParams }) + + expect(onUnknownParams).toHaveBeenCalledTimes(1) + expect(onUnknownParams).toHaveBeenCalledWith(['mystery'], params) + }) + + it('does not mutate params or nested objects', () => { + const params = { query: { a: 1 }, provider: 'rest', foo: 1 } as any + const snapshot = structuredClone(params) + + passParams(params, { query: true, foo: false }) + passParams(params, { 'query.a': true }) + + expect(params).toEqual(snapshot) + }) +}) diff --git a/src/utils/pass-params/pass-params.util.ts b/src/utils/pass-params/pass-params.util.ts new file mode 100644 index 0000000..ce9aa1c --- /dev/null +++ b/src/utils/pass-params/pass-params.util.ts @@ -0,0 +1,169 @@ +import type { Params } from '@feathersjs/feathers' +import _get from 'lodash/get.js' +import _has from 'lodash/has.js' +import _set from 'lodash/set.js' +import _toPath from 'lodash/toPath.js' + +/** + * A rule for a single schema path. + * - `true` → include the value as-is + * - `false` → drop the path + * - function → unified predicate/projection (see {@link PassParamsFn}) + */ +export type PassParamsRule

= + | boolean + | PassParamsFn

+ +/** + * The flexible slot — serves as BOTH predicate and projection: + * - returns `true` → include the value as-is + * - returns `false` | `undefined` → drop the path + * - returns any other value `r` → include `r` (projection) + * + * @example predicate: `(value) => value != null` + * @example projection: `(user) => user?.id` + */ +export type PassParamsFn

= ( + value: any, + params: P, +) => boolean | undefined | unknown + +/** + * Declarative schema keyed by lodash **paths** in dot-notation + * (e.g. `'query'`, `'user.id'`, `'authentication.payload.sub'`). Paths are read + * with lodash `get`/`has`; the result is built with `set` at the same path. + * Any custom path is allowed. + */ +export type PassParamsSchema

= Record< + string, + PassParamsRule

+> + +export type PassParamsOptions

= { + /** + * Drop top-level `params` keys that the schema does not address (neither as a + * top-level key nor as the root of a nested path). When `false` (the default) + * those keys are kept, so only the ones you explicitly list with `false` are + * removed; when `true` only `query` and the schema paths remain. + * + * Keeping unknown keys (the default) is the safe direction for cache keys: a + * forgotten key causes at worst a harmless cache miss, never a false hit. + * + * @default false + */ + dropUnknownParams?: boolean + /** + * Optional observer, invoked once with the top-level `params` keys the schema + * did not address (skipped when there are none). The place to `logger.warn(...)` + * about keys you never classified so overlooked params become loud instead of + * silently mis-caching. Purely observational — it does not change what is kept. + * + * @example `(keys) => keys.forEach((k) => logger.warn('undeclared cache param', k))` + */ + onUnknownParams?: (keys: string[], params: P) => void +} + +/** + * Selects and/or projects `params` keys according to a declarative path `schema`, + * returning a NEW object (never mutates `params`). General-purpose — no cache + * knowledge. Typically composed into the cache hook's `transformParams` option + * as `(p) => passParams(p, schema, opts)`. + * + * Paths are resolved with lodash `get`/`has` and written with `set`, so nested + * values can be picked declaratively (`'user.id': true`). + * + * `query` is included as-is by DEFAULT (it is always relevant), unless the schema + * addresses it explicitly — either as `query` or a nested `query.*` path. + * + * Keys not mentioned in the schema are KEPT by default, so forgetting a relevant + * key can only cause a harmless cache miss, never a false hit. Pass + * `dropUnknownParams: true` to keep only `query` and the schema paths. + * + * @example exclude specific params (default): everything except the listed noise + * ```ts + * passParams(params, { rateLimit: false }) + * ``` + * @example include only specific params + * ```ts + * passParams(params, { 'user.id': true }, { dropUnknownParams: true }) + * ``` + * + * @see https://utils.feathersjs.com/utils/pass-params.html + */ +export function passParams

( + params: P, + schema: PassParamsSchema

, + options?: PassParamsOptions

, +): Params { + const out: Record = {} + const claimedTop = new Set() + + // schema paths drive inclusion / projection. + for (const path of Object.keys(schema)) { + // A path claims its top-level segment, so a parent whose child was declared + // is never treated as unknown (e.g. `user.id` claims `user`). + const topKey = _toPath(path)[0] + if (topKey !== undefined) { + claimedTop.add(topKey) + } + + const rule = schema[path] + + // explicit exclude — no need to read params + if (rule === false) { + continue + } + + // path not present — nothing to include (no injection) + if (!_has(params, path)) { + continue + } + + const value = _get(params, path) + + // dynamic predicate / projection + if (typeof rule === 'function') { + const result = rule(value, params) + if (result === false || result === undefined) { + continue + } + _set(out, path, result === true ? value : result) + continue + } + + // rule === true → include as-is + _set(out, path, value) + } + + // `query` is always relevant, so it is included as-is by default — UNLESS the + // schema addresses it explicitly (either as `query` or a nested `query.*` path, + // both of which add `query` to `claimedTop`). This keeps `query` out of the + // unknown-key handling and prevents accidentally caching across queries. + if (!claimedTop.has('query')) { + claimedTop.add('query') + if (_has(params, 'query')) { + out.query = _get(params, 'query') + } + } + + // Top-level keys not addressed by the schema are kept by default; `dropUnknownParams` + // removes them instead. Either way they are collected for the `onUnknownParams` observer. + const dropUnknown = options?.dropUnknownParams ?? false + const source = params as Record + const unknownKeys: string[] = [] + for (const key of Object.keys(source)) { + if (claimedTop.has(key)) { + continue + } + unknownKeys.push(key) + if (!dropUnknown) { + out[key] = source[key] + } + } + + if (unknownKeys.length && options?.onUnknownParams) { + options.onUnknownParams(unknownKeys, params) + } + + return out +} diff --git a/test/index.test.ts b/test/index.test.ts index 7f880e5..2efa445 100755 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -60,6 +60,7 @@ const utils = [ 'mergeQuery', 'mutateData', 'mutateResult', + 'passParams', 'patchBatch', 'queryDefaults', 'queryHasProperty', From de7792ba8b8c01f44d0fce14606671424c744171 Mon Sep 17 00:00:00 2001 From: fratzinger <22286818+fratzinger@users.noreply.github.com> Date: Wed, 1 Jul 2026 10:01:31 +0200 Subject: [PATCH 2/2] docs: cache hook add see & other improvements --- src/hooks/cache/cache.hook.md | 98 ++++++++++++++++++----------------- src/hooks/cache/cache.hook.ts | 14 +++++ 2 files changed, 64 insertions(+), 48 deletions(-) diff --git a/src/hooks/cache/cache.hook.md b/src/hooks/cache/cache.hook.md index 14d13f3..388ac27 100644 --- a/src/hooks/cache/cache.hook.md +++ b/src/hooks/cache/cache.hook.md @@ -5,6 +5,8 @@ hook: type: ['before', 'after'] method: ['find', 'get', 'create', 'update', 'patch', 'remove'] multi: true +see: + - utils/passParams --- The `cache` hook caches `get` and `find` results based on `params`. On mutating methods (`create`, `update`, `patch`, `remove`), affected cache entries are automatically invalidated. @@ -21,6 +23,53 @@ The `cache` hook caches `get` and `find` results based on `params`. On mutating | `id` | `string` | The id field to use. Defaults to `service.options.id`, then `'id'`. | | `transformParams` | `(params) => params` | Transform params before they are used as cache key. Compose it with [`passParams`](/utils/pass-params) to declaratively pick/drop keys and avoid false hits — see [Choosing Cache-Relevant Params](#choosing-cache-relevant-params-with-passparams). | +## Choosing Cache-Relevant Params (with `passParams`) + +Deciding which `params` keys form the cache key is the trickiest part of caching, and the two failure modes are asymmetric: + +- **False hits (dangerous):** if a key that affects the result is left out (e.g. `user`/tenant, `provider`), two semantically different requests collapse to the same key — one user can be served another user's cached data. +- **False misses (wasteful):** if a per-request/metrics key is included (e.g. `rateLimit`), every request produces a unique key and the cache never hits. A function-valued key (e.g. `stashed` from `stashable`) would even make serialization throw. + +The [`passParams`](/utils/pass-params) utility makes this explicit and safe. It takes a declarative path schema (`true` include, `false` drop, or a predicate/projection function). `query` is always included by default, and keys you never classified are **kept by default** — the safe direction, since a forgotten key causes at worst a harmless cache miss, never a false hit. + +> Transient keys that feathers-utils' own hooks attach to `params` — `rateLimit` (`rateLimit`), `skipHooks` (`skippable`/`addSkip`), the `stashed` function and `_stashable` flag (`stashable`) — are never cache-relevant. Drop them with `false`, or keep only what you list via `dropUnknownParams: true`. + +### Exclude specific params (default) + +Cache on everything except the keys you explicitly drop with `false`. This is the default direction — safe against false hits: + +```ts +import { passParams } from 'feathers-utils/utils' + +cache({ + map: new Map(), + transformParams: (params) => + passParams(params, { rateLimit: false, skipHooks: false }), +}) +``` + +### Include only specific params + +Set `dropUnknownParams: true` so only `query` (always) and the listed paths form the cache key. `user.id` is picked via dot-notation so different tenants never collide and per-request `user` fields don't bloat the key. Use `onUnknownParams` to log anything that was dropped: + +```ts +import { passParams } from 'feathers-utils/utils' + +cache({ + map: new Map(), + transformParams: (params) => + passParams( + params, + { 'user.id': true }, // `query` is included automatically + { + dropUnknownParams: true, + onUnknownParams: (keys) => + keys.forEach((key) => logger.warn('undeclared cache param', key)), + }, + ), +}) +``` + ## Cache Interface Any object that implements the following interface can be used as a cache: @@ -37,7 +86,7 @@ type Cache = { A plain `Map` satisfies this interface out of the box, as do many popular cache libraries. -## Examples +## Examples of storages ### Basic Usage with Map @@ -211,50 +260,3 @@ app.service('users').hooks({ }, }) ``` - -### Choosing Cache-Relevant Params (with `passParams`) - -Deciding which `params` keys form the cache key is the trickiest part of caching, and the two failure modes are asymmetric: - -- **False hits (dangerous):** if a key that affects the result is left out (e.g. `user`/tenant, `provider`), two semantically different requests collapse to the same key — one user can be served another user's cached data. -- **False misses (wasteful):** if a per-request/metrics key is included (e.g. `rateLimit`), every request produces a unique key and the cache never hits. A function-valued key (e.g. `stashed` from `stashable`) would even make serialization throw. - -The [`passParams`](/utils/pass-params) utility makes this explicit and safe. It takes a declarative path schema (`true` include, `false` drop, or a predicate/projection function). `query` is always included by default, and keys you never classified are **kept by default** — the safe direction, since a forgotten key causes at worst a harmless cache miss, never a false hit. - -> Transient keys that feathers-utils' own hooks attach to `params` — `rateLimit` (`rateLimit`), `skipHooks` (`skippable`/`addSkip`), the `stashed` function and `_stashable` flag (`stashable`) — are never cache-relevant. Drop them with `false`, or keep only what you list via `dropUnknownParams: true`. - -#### Exclude specific params (default) - -Cache on everything except the keys you explicitly drop with `false`. This is the default direction — safe against false hits: - -```ts -import { passParams } from 'feathers-utils/utils' - -cache({ - map: new Map(), - transformParams: (params) => - passParams(params, { rateLimit: false, skipHooks: false }), -}) -``` - -#### Include only specific params - -Set `dropUnknownParams: true` so only `query` (always) and the listed paths form the cache key. `user.id` is picked via dot-notation so different tenants never collide and per-request `user` fields don't bloat the key. Use `onUnknownParams` to log anything that was dropped: - -```ts -import { passParams } from 'feathers-utils/utils' - -cache({ - map: new Map(), - transformParams: (params) => - passParams( - params, - { 'user.id': true }, // `query` is included automatically - { - dropUnknownParams: true, - onUnknownParams: (keys) => - keys.forEach((key) => logger.warn('undeclared cache param', key)), - }, - ), -}) -``` diff --git a/src/hooks/cache/cache.hook.ts b/src/hooks/cache/cache.hook.ts index 0f60d6f..6e5d52a 100755 --- a/src/hooks/cache/cache.hook.ts +++ b/src/hooks/cache/cache.hook.ts @@ -34,6 +34,20 @@ export type CacheOptions = { * params are stringified for the key-value cache. * There are params properties you don't want to include in the cache key. * You can use this function to transform the params before they are stringified. + * + * The {@link passParams} util is built for exactly this: it declaratively + * selects/projects `params` keys (keeping `query` by default) so noise like + * `rateLimit` never ends up in the cache key. + * + * @example + * ```ts + * import { passParams } from 'feathers-utils/utils' + * + * cache({ + * map: new Map(), + * transformParams: (params) => passParams(params, { rateLimit: false }), + * }) + * ``` */ transformParams: (params: Params) => Params /**