├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github ├── contributing.md ├── issue_template.md ├── pull_request_template.md └── workflows │ └── test.yml ├── .gitignore ├── .istanbul.yml ├── .mocharc.json ├── .npmignore ├── .travis.yml ├── .vscode.tmpl ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── dist ├── constants.d.ts ├── constants.js ├── error-handler.d.ts ├── error-handler.js ├── index.d.ts ├── index.js ├── service.d.ts ├── service.js ├── types.d.ts ├── types.js ├── utils.d.ts └── utils.js ├── mocha.opts ├── package-lock.json ├── package.json ├── prisma ├── migrations │ ├── 20220107090636_init │ │ └── migration.sql │ └── migration_lock.toml ├── schema.prisma └── tests.db ├── src ├── constants.ts ├── error-handler.ts ├── index.ts ├── service.ts ├── types.ts └── utils.ts ├── test ├── errors.test.js └── index.test.js └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # * 2 | lib/ 3 | types/ 4 | node_modules 5 | !src/ 6 | example/ 7 | dist/ 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'env': { 3 | 'es2021': true, 4 | 'node': true, 5 | 'mocha': true, 6 | }, 7 | 'extends': [ 8 | 'eslint:recommended', 9 | 'plugin:@typescript-eslint/recommended' 10 | ], 11 | 'parser': '@typescript-eslint/parser', 12 | 'parserOptions': { 13 | 'ecmaVersion': 12, 14 | 'sourceType': 'module' 15 | }, 16 | 'plugins': [ 17 | '@typescript-eslint' 18 | ], 19 | 'rules': { 20 | '@typescript-eslint/ban-ts-comment': 0, 21 | '@typescript-eslint/no-explicit-any': 0, 22 | '@typescript-eslint/indent': ['error', 2], 23 | 'indent': 'off', 24 | 'linebreak-style': [ 25 | 'error', 26 | 'unix' 27 | ], 28 | 'quotes': [ 29 | 'error', 30 | 'single' 31 | ], 32 | 'semi': [ 33 | 'error', 34 | 'always' 35 | ], 36 | 'import/no-import-module-exports': 'off', 37 | 'no-underscore-dangle': 'off', 38 | 'max-len': ['warn', { code: 120 }], 39 | }, 40 | overrides: [ 41 | { 42 | files: ['*.js'], 43 | rules: { 44 | '@typescript-eslint/no-var-requires': 'off', 45 | }, 46 | }, 47 | ], 48 | }; 49 | -------------------------------------------------------------------------------- /.github/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to Feathers 2 | 3 | Thank you for contributing to Feathers! :heart: :tada: 4 | 5 | This repo is the main core and where most issues are reported. Feathers embraces modularity and is broken up across many repos. 6 | 7 | ## Report a bug 8 | 9 | Before creating an issue please make sure you have checked out the docs, specifically the [FAQ](https://docs.feathersjs.com/help/faq.html) section. You might want to also try searching Github. It's pretty likely someone has already asked a similar question. 10 | 11 | If you haven't found your answer please feel free to join our [slack channel](http://slack.feathersjs.com), create an issue on Github, or post on [Stackoverflow](http://stackoverflow.com) using the `feathers` or `feathersjs` tag. We try our best to monitor Stackoverflow but you're likely to get more immediate responses in Slack and Github. 12 | 13 | Issues can be reported in the [issue tracker](https://github.com/feathersjs/feathers/issues). Since feathers combines many modules it can be hard for us to assess the root cause without knowing which modules are being used and what your configuration looks like, so **it helps us immensely if you can link to a simple example that reproduces your issue**. 14 | 15 | ## Report a Security Concern 16 | 17 | We take security very seriously at Feathers. We welcome any peer review of our 100% open source code to ensure nobody's Feathers app is ever compromised or hacked. As a web application developer you are responsible for any security breaches. We do our very best to make sure Feathers is as secure as possible by default. 18 | 19 | In order to give the community time to respond and upgrade we strongly urge you report all security issues to us. Send one of the core team members a PM in [Slack](http://slack.feathersjs.com) or email us at hello@feathersjs.com with details and we will respond ASAP. 20 | 21 | For full details refer to our [Security docs](https://docs.feathersjs.com/SECURITY.html). 22 | 23 | ## Pull Requests 24 | 25 | We :heart: pull requests and we're continually working to make it as easy as possible for people to contribute, including a [Plugin Generator](https://github.com/feathersjs/generator-feathers-plugin) and a [common test suite](https://github.com/feathersjs/feathers-service-tests) for database adapters. 26 | 27 | We prefer small pull requests with minimal code changes. The smaller they are the easier they are to review and merge. A core team member will pick up your PR and review it as soon as they can. They may ask for changes or reject your pull request. This is not a reflection of you as an engineer or a person. Please accept feedback graciously as we will also try to be sensitive when providing it. 28 | 29 | Although we generally accept many PRs they can be rejected for many reasons. We will be as transparent as possible but it may simply be that you do not have the same context or information regarding the roadmap that the core team members have. We value the time you take to put together any contributions so we pledge to always be respectful of that time and will try to be as open as possible so that you don't waste it. :smile: 30 | 31 | **All PRs (except documentation) should be accompanied with tests and pass the linting rules.** 32 | 33 | ### Code style 34 | 35 | Before running the tests from the `test/` folder `npm test` will run ESlint. You can check your code changes individually by running `npm run lint`. 36 | 37 | ### Tests 38 | 39 | [Mocha](http://mochajs.org/) tests are located in the `test/` folder and can be run using the `npm run mocha` or `npm test` (with ESLint and code coverage) command. 40 | 41 | ## External Modules 42 | 43 | If you're written something awesome for Feathers, the Feathers ecosystem, or using Feathers please add it to the [awesome-feathersjs](https://github.com/feathersjs/awesome-feathersjs) repository. 44 | 45 | If you think it would be a good core module then please contact one of the Feathers core team members in [Slack](http://slack.feathersjs.com) and we can discuss whether it belongs in core and how to get it there. :beers: 46 | 47 | ## Contributor Code of Conduct 48 | 49 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 50 | 51 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. 52 | 53 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 54 | 55 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 58 | 59 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) 60 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | ### Steps to reproduce 2 | 3 | (First please check that this issue is not already solved as [described 4 | here](https://github.com/feathersjs/feathers/blob/master/.github/contributing.md#report-a-bug)) 5 | 6 | - [ ] Tell us what broke. The more detailed the better. 7 | - [ ] If you can, please create a simple example that reproduces the issue and link to a gist, jsbin, repo, etc. 8 | 9 | ### Expected behavior 10 | 11 | Tell us what should happen 12 | 13 | ### Actual behavior 14 | 15 | Tell us what happens instead 16 | 17 | ### System configuration 18 | 19 | Tell us about the applicable parts of your setup. 20 | 21 | **Module versions** (especially the part that's not working): 22 | 23 | **NodeJS version**: 24 | 25 | **Operating System**: 26 | 27 | **Browser Version**: 28 | 29 | **React Native Version**: 30 | 31 | **Module Loader**: 32 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Summary 2 | 3 | - [ ] Tell us about the problem your pull request is solving. 4 | - [ ] Are there any open issues that are related to this? 5 | - [ ] Is this PR dependent on PRs in other repos? 6 | 7 | If so, please mention them to keep the conversations linked together. 8 | 9 | ### Other Information 10 | 11 | If there's anything else that's important and relevant to your pull 12 | request, mention that information here. This could include 13 | benchmarks, or other information. 14 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: 4 | push: 5 | branches: [ '*' ] 6 | pull_request: 7 | branches: ['*'] 8 | jobs: 9 | build: 10 | name: 'Run tests' 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [16, 18, 20] 16 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: restore caches 21 | uses: actions/cache@v2 22 | with: 23 | path: | 24 | ./node_modules 25 | ./.coverage 26 | ./.eslintcache 27 | key: ${{ runner.os }}-${{ matrix.node-version }}-${{ hashFiles('package-lock.json') }} 28 | - name: Run tests 29 | uses: actions/setup-node@v2 30 | with: 31 | node-version: ${{ matrix.node-version }} 32 | cache: 'npm' 33 | - run: npm install && npx prisma generate && npm run lint -- --cache && npm run test 34 | - name: Publish code coverage 35 | uses: paambaati/codeclimate-action@v3.0.0 36 | env: 37 | CC_TEST_REPORTER_ID: ${{ secrets.CODE_CLIMATE_REPORTER_ID }} 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Logs 4 | logs 5 | *.log 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # Compiled binary addons (http://nodejs.org/api/addons.html) 22 | build/Release 23 | 24 | # Dependency directory 25 | # Commenting this out is preferred by some people, see 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 27 | node_modules 28 | 29 | # Users Environment Variables 30 | .lock-wscript 31 | 32 | .env 33 | 34 | .vscode 35 | 36 | .eslintcache 37 | -------------------------------------------------------------------------------- /.istanbul.yml: -------------------------------------------------------------------------------- 1 | verbose: false 2 | instrumentation: 3 | root: ./dist/ 4 | include-all-sources: true 5 | reporting: 6 | print: summary 7 | reports: 8 | - html 9 | - text 10 | - lcov 11 | watermarks: 12 | statements: [50, 80] 13 | lines: [50, 80] 14 | functions: [50, 80] 15 | branches: [50, 80] 16 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "recursive": true, 3 | "spec": ["test/**/*.test.js"] 4 | } 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .editorconfig 2 | .jshintrc 3 | .travis.yml 4 | .istanbul.yml 5 | .babelrc 6 | .idea/ 7 | .vscode/ 8 | test/ 9 | coverage/ 10 | .github/ 11 | example/ 12 | prisma/ 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'node' 4 | - '6' -------------------------------------------------------------------------------- /.vscode.tmpl/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.vscode.tmpl/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.workingDirectories": ["./", "./example"], 3 | "[javascript]": { 4 | "editor.formatOnPaste": false, 5 | "editor.formatOnSave": false, 6 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 7 | }, 8 | "javascript.format.enable": false, 9 | "javascript.validate.enable": true, 10 | "javascript.updateImportsOnFileMove.enabled": "prompt", 11 | "javascript.suggestionActions.enabled": false, 12 | "editor.formatOnPaste": true, 13 | "editor.formatOnSave": true, 14 | "editor.codeActionsOnSave": { 15 | "source.fixAll.eslint": true, 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Feathers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # feathers-prisma 2 | 3 | [![libraries.io](https://img.shields.io/librariesio/release/npm/feathers-prisma)](https://libraries.io/npm/feathers-prisma) 4 | [![Code Climate](https://codeclimate.com/github/ps73/feathers-prisma/badges/gpa.svg)](https://codeclimate.com/github/ps73/feathers-prisma) 5 | [![Test Coverage](https://codeclimate.com/github/ps73/feathers-prisma/badges/coverage.svg)](https://codeclimate.com/github/ps73/feathers-prisma/coverage) 6 | [![npm](https://img.shields.io/npm/v/feathers-prisma.svg?maxAge=3600)](https://www.npmjs.com/package/feathers-prisma) 7 | 8 | > A [Feathers](https://feathersjs.com) service adapter for [Prisma](prisma.io) ORM. 9 | 10 | ## Installation 11 | 12 | ``` 13 | npm install feathers-prisma --save 14 | ``` 15 | 16 | ## Documentation 17 | 18 | This adapter supports all methods (`create`, `delete`, `update`, `patch`, `find`, `get`) and the common way for querying (`equality`, `$limit`, `$skip`, `$sort`, `$select`, `$in`, `$nin`, `$lt`, `$lte`, `$gt`, `$gte`, `$ne`, `$or`, `$and`). Also supports eager loading (`$eager`), full-text search (`$search`) and prisma filtering (from 0.7.0 on with `$prisma`, previously with `$rawWhere` which is now deprecated). 19 | 20 | ## Prisma Version 21 | 22 | - Prisma v3 use `feathers-prisma` v0.6.0 23 | - Prisma v5 use `feathers-prisma` v0.7.0 or higher 24 | 25 | ### Setup 26 | 27 | ```js 28 | import feathers from "@feathersjs/feathers"; 29 | import { service } from "feathers-prisma"; 30 | import { PrismaClient } from "@prisma/client"; 31 | 32 | // Initialize the application 33 | const app = feathers(); 34 | 35 | // Initialize the plugin 36 | const prismaClient = new PrismaClient(); 37 | prismaClient.$connect(); 38 | app.set("prisma", prismaClient); 39 | 40 | const paginate = { 41 | default: 10, 42 | max: 50, 43 | }; 44 | 45 | app.use( 46 | "/messages", 47 | service( 48 | { 49 | model: "messages", 50 | paginate, 51 | multi: ["create", "patch", "remove"], 52 | whitelist: ["$eager"], 53 | }, 54 | prismaClient 55 | ) 56 | ); 57 | ``` 58 | 59 | ### Eager Loading / Relation Queries 60 | 61 | Relations can be resolved via `$eager` property in your query. It supports also deep relations. The `$eager` property **has to be** set in the `whitelist` option parameter. Otherwise the service will throw an error. 62 | 63 | ```js 64 | app.use( 65 | "/messages", 66 | service( 67 | { 68 | model: "message", 69 | whitelist: ["$eager"], 70 | }, 71 | prismaClient 72 | ) 73 | ); 74 | // will load the recipients with the related user 75 | // as well as all attachments of the messages 76 | app.service("messages").find({ 77 | query: { 78 | $eager: [["recipients", ["user"]], "attachments"], 79 | }, 80 | }); 81 | // selecting specific fields is also supported since 0.4.0 82 | app.service("messages").find({ 83 | query: { 84 | $eager: { 85 | recipients: ["receivedAt", "user"], 86 | }, 87 | }, 88 | }); 89 | ``` 90 | 91 | ### Filter with default prisma filters 92 | 93 | Since 0.5.0 it is possible to use default prisma filters. This makes it possible to [filter JSON](https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields) fields or to [filter relations](https://www.prisma.io/docs/concepts/components/prisma-client/relation-queries#relation-filters). 94 | 95 | The `$prisma` property **has to be** set in the `whitelist` option parameter. Otherwise the service will throw an error. 96 | 97 | Since 0.7.0 use the $prisma property to filter instead of using the $rawWhere property. 98 | 99 | ```js 100 | app.use( 101 | "/messages", 102 | service( 103 | { 104 | model: "message", 105 | whitelist: ["$prisma"], 106 | }, 107 | prismaClient 108 | ) 109 | ); 110 | // will load all messages where at least one of the recipients userIds is equal 1 111 | app.service("messages").find({ 112 | query: { 113 | recipients: { 114 | $prisma: { 115 | some: { 116 | userId: 1, 117 | }, 118 | }, 119 | }, 120 | }, 121 | }); 122 | ``` 123 | 124 | ### Batch requests 125 | 126 | This adapter supports batch requests. This is possible by allowing this in the `multi` property in the service options. Supported methods are `create`, `patch` and `delete`. 127 | 128 | ```js 129 | app.use( 130 | "/messages", 131 | service( 132 | { 133 | model: "messages", 134 | multi: ["create", "patch", "delete"], 135 | }, 136 | prismaClient 137 | ) 138 | ); 139 | 140 | app.service("messages").create([{ body: "Lorem" }, { body: "Ipsum" }]); 141 | ``` 142 | 143 | ### Full-Text Search 144 | 145 | Prisma supports a full-text search which is currently in preview mode. Find out more how to activate it [here](https://www.prisma.io/docs/concepts/components/prisma-client/full-text-search). If you activated it through your schema you have to allow it in the `whitelist` property: 146 | 147 | ```js 148 | app.use( 149 | "/messages", 150 | service( 151 | { 152 | model: "messages", 153 | whitelist: ["$search"], 154 | }, 155 | prismaClient 156 | ) 157 | ); 158 | 159 | app.service("messages").find({ 160 | query: { 161 | body: { 162 | $search: "hello | hola", 163 | }, 164 | }, 165 | }); 166 | ``` 167 | 168 | ## Complete Example 169 | 170 | Here's an example of a Feathers server that uses `feathers-prisma`. 171 | 172 | ```js 173 | import feathers from "@feathersjs/feathers"; 174 | import { service } from "feathers-prisma"; 175 | 176 | // Initialize the application 177 | const app = feathers(); 178 | 179 | // Initialize the plugin 180 | const prismaClient = new PrismaClient(); 181 | prismaClient.$connect(); 182 | app.set("prisma", prismaClient); 183 | 184 | const paginate = { 185 | default: 10, 186 | max: 50, 187 | }; 188 | 189 | app.use( 190 | "/messages", 191 | service( 192 | { 193 | model: "messages", 194 | paginate, 195 | multi: ["create", "patch", "remove"], 196 | whitelist: ["$eager"], 197 | }, 198 | prismaClient 199 | ) 200 | ); 201 | ``` 202 | 203 | ```js 204 | // Or if you want to extend the service class 205 | import { PrismaService } from "feathers-prisma"; 206 | ``` 207 | 208 | ## License 209 | 210 | Copyright (c) 2021. 211 | 212 | Licensed under the [MIT license](LICENSE). 213 | -------------------------------------------------------------------------------- /dist/constants.d.ts: -------------------------------------------------------------------------------- 1 | export declare const OPERATORS: { 2 | not: string; 3 | gte: string; 4 | gt: string; 5 | lte: string; 6 | lt: string; 7 | in: string; 8 | notIn: string; 9 | and: string; 10 | or: string; 11 | contains: string; 12 | startsWith: string; 13 | endsWith: string; 14 | mode: string; 15 | }; 16 | export declare const OPERATORS_MAP: { 17 | $lt: string; 18 | $lte: string; 19 | $gt: string; 20 | $gte: string; 21 | $in: string; 22 | $nin: string; 23 | $ne: string; 24 | $eager: string; 25 | /** 26 | * @deprecated use $prisma instead 27 | */ 28 | $rawWhere: string; 29 | $prisma: string; 30 | $contains: string; 31 | $search: string; 32 | $startsWith: string; 33 | $endsWith: string; 34 | $mode: string; 35 | }; 36 | -------------------------------------------------------------------------------- /dist/constants.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.OPERATORS_MAP = exports.OPERATORS = void 0; 4 | exports.OPERATORS = { 5 | not: '$ne', 6 | gte: '$gte', 7 | gt: '$gt', 8 | lte: '$lte', 9 | lt: '$lt', 10 | in: '$in', 11 | notIn: '$nin', 12 | and: '$and', 13 | or: '$or', 14 | // specific to prisma 15 | contains: '$contains', 16 | startsWith: '$startsWith', 17 | endsWith: '$endsWith', 18 | mode: '$mode', 19 | }; 20 | exports.OPERATORS_MAP = { 21 | $lt: 'lt', 22 | $lte: 'lte', 23 | $gt: 'gt', 24 | $gte: 'gte', 25 | $in: 'in', 26 | $nin: 'notIn', 27 | $ne: 'not', 28 | $eager: 'includes', 29 | // specific to prisma 30 | /** 31 | * @deprecated use $prisma instead 32 | */ 33 | $rawWhere: 'rawWhere', 34 | $prisma: 'prisma', 35 | $contains: 'contains', 36 | $search: 'search', 37 | $startsWith: 'startsWith', 38 | $endsWith: 'endsWith', 39 | $mode: 'mode', 40 | }; 41 | -------------------------------------------------------------------------------- /dist/error-handler.d.ts: -------------------------------------------------------------------------------- 1 | export declare function errorHandler(error: any, prismaMethod?: string): void; 2 | -------------------------------------------------------------------------------- /dist/error-handler.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.errorHandler = void 0; 4 | const errors = require("@feathersjs/errors"); 5 | const library_1 = require("@prisma/client/runtime/library"); 6 | function getType(v) { 7 | let type = ''; 8 | const cases = { 9 | common: v >= 1000 && v < 2000, 10 | query: v >= 2000 && v < 3000, 11 | migration: v >= 3000 && v < 4000, 12 | introspection: v >= 4000 && v < 5000, 13 | }; 14 | Object.keys(cases).map((key) => { 15 | // @ts-ignore 16 | if (cases[key]) { 17 | type = key; 18 | } 19 | return key; 20 | }); 21 | return type; 22 | } 23 | function errorHandler(error, prismaMethod) { 24 | let feathersError; 25 | if (error instanceof errors.FeathersError) { 26 | feathersError = error; 27 | } 28 | else if (error instanceof library_1.PrismaClientKnownRequestError) { 29 | const { code, meta, message, clientVersion, } = error; 30 | const errType = getType(Number(code.substring(1))); 31 | switch (errType) { 32 | case 'common': 33 | feathersError = new errors.GeneralError(message, { code, meta, clientVersion }); 34 | break; 35 | case 'query': 36 | feathersError = new errors.BadRequest(message, { code, meta, clientVersion }); 37 | if (code === 'P2025') { 38 | // @ts-ignore 39 | feathersError = new errors.NotFound((meta === null || meta === void 0 ? void 0 : meta.cause) || 'Record not found.'); 40 | } 41 | break; 42 | case 'migration': 43 | feathersError = new errors.GeneralError(message, { code, meta, clientVersion }); 44 | break; 45 | case 'introspection': 46 | feathersError = new errors.GeneralError(message, { code, meta, clientVersion }); 47 | break; 48 | default: 49 | feathersError = new errors.BadRequest(message, { code, meta, clientVersion }); 50 | break; 51 | } 52 | } 53 | else if (error instanceof library_1.PrismaClientValidationError) { 54 | switch (prismaMethod) { 55 | case 'findUnique': 56 | case 'remove': 57 | case 'update': 58 | case 'delete': 59 | feathersError = new errors.NotFound('Record not found.'); 60 | break; 61 | default: 62 | feathersError = new errors.GeneralError(error); 63 | break; 64 | } 65 | } 66 | else { 67 | feathersError = new errors.GeneralError(error); 68 | } 69 | throw feathersError; 70 | } 71 | exports.errorHandler = errorHandler; 72 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | export { service, prismaService, PrismaService } from './service'; 2 | export * from './types'; 3 | export * from './constants'; 4 | export * from './error-handler'; 5 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | var desc = Object.getOwnPropertyDescriptor(m, k); 5 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 6 | desc = { enumerable: true, get: function() { return m[k]; } }; 7 | } 8 | Object.defineProperty(o, k2, desc); 9 | }) : (function(o, m, k, k2) { 10 | if (k2 === undefined) k2 = k; 11 | o[k2] = m[k]; 12 | })); 13 | var __exportStar = (this && this.__exportStar) || function(m, exports) { 14 | for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); 15 | }; 16 | Object.defineProperty(exports, "__esModule", { value: true }); 17 | exports.PrismaService = exports.prismaService = exports.service = void 0; 18 | var service_1 = require("./service"); 19 | Object.defineProperty(exports, "service", { enumerable: true, get: function () { return service_1.service; } }); 20 | Object.defineProperty(exports, "prismaService", { enumerable: true, get: function () { return service_1.prismaService; } }); 21 | Object.defineProperty(exports, "PrismaService", { enumerable: true, get: function () { return service_1.PrismaService; } }); 22 | __exportStar(require("./types"), exports); 23 | __exportStar(require("./constants"), exports); 24 | __exportStar(require("./error-handler"), exports); 25 | -------------------------------------------------------------------------------- /dist/service.d.ts: -------------------------------------------------------------------------------- 1 | import type { Params } from '@feathersjs/feathers'; 2 | import { AdapterService } from '@feathersjs/adapter-commons'; 3 | import { PrismaClient } from '@prisma/client'; 4 | import { IdField, PrismaServiceOptions } from './types'; 5 | export declare class PrismaService> extends AdapterService { 6 | Model: any; 7 | client: PrismaClient; 8 | constructor(options: PrismaServiceOptions, client: PrismaClient); 9 | _find(params?: Params): Promise; 10 | _get(id: IdField, params?: Params): Promise | undefined>; 11 | _create(data: Partial | Partial[], params?: Params): Promise | Partial[] | undefined>; 12 | _update(id: IdField, data: Partial, params?: Params, returnResult?: boolean): Promise; 13 | _patch(id: IdField | null, data: Partial | Partial[], params?: Params): Promise; 14 | _remove(id: IdField | null, params?: Params): Promise; 15 | } 16 | export declare function service>(options: PrismaServiceOptions, client: PrismaClient): PrismaService; 17 | export declare const prismaService: typeof service; 18 | -------------------------------------------------------------------------------- /dist/service.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | exports.prismaService = exports.service = exports.PrismaService = void 0; 13 | const adapter_commons_1 = require("@feathersjs/adapter-commons"); 14 | const errors = require("@feathersjs/errors"); 15 | const utils_1 = require("./utils"); 16 | const constants_1 = require("./constants"); 17 | const error_handler_1 = require("./error-handler"); 18 | class PrismaService extends adapter_commons_1.AdapterService { 19 | constructor(options, client) { 20 | var _a, _b; 21 | super({ 22 | id: options.id || 'id', 23 | paginate: { 24 | default: (_a = options.paginate) === null || _a === void 0 ? void 0 : _a.default, 25 | max: (_b = options.paginate) === null || _b === void 0 ? void 0 : _b.max, 26 | }, 27 | multi: options.multi || [], 28 | filters: options.filters || [], 29 | events: options.events || [], 30 | whitelist: Object.values(constants_1.OPERATORS).concat(options.whitelist || []), 31 | }); 32 | const { model } = options; 33 | if (!model) { 34 | throw new errors.GeneralError('You must provide a model string.'); 35 | } 36 | // @ts-ignore 37 | if (!client[model]) { 38 | throw new errors.GeneralError(`No model with name ${model} found in prisma client.`); 39 | } 40 | this.client = client; 41 | // @ts-ignore 42 | this.Model = client[model]; 43 | } 44 | _find(params = {}) { 45 | return __awaiter(this, void 0, void 0, function* () { 46 | const { query, filters } = this.filterQuery(params); 47 | const { whitelist } = this.options; 48 | const { skip, take, orderBy, where, select, include } = (0, utils_1.buildPrismaQueryParams)({ 49 | query, filters, whitelist, 50 | }, this.options.id); 51 | try { 52 | const findMany = () => { 53 | return this.Model.findMany(Object.assign(Object.assign(Object.assign({}, (typeof take === 'number' ? { skip, take } : { skip })), { orderBy, 54 | where }), (0, utils_1.buildSelectOrInclude)({ select, include }))); 55 | }; 56 | if (!this.options.paginate.default || (typeof take !== 'number' && !take)) { 57 | const data = yield findMany(); 58 | return data; 59 | } 60 | const [data, count] = yield this.client.$transaction([ 61 | findMany(), 62 | this.Model.count({ 63 | where, 64 | }), 65 | ]); 66 | const result = { 67 | total: count, 68 | skip, 69 | limit: take, 70 | data, 71 | }; 72 | return result; 73 | } 74 | catch (e) { 75 | (0, error_handler_1.errorHandler)(e); 76 | } 77 | }); 78 | } 79 | _get(id, params = {}) { 80 | return __awaiter(this, void 0, void 0, function* () { 81 | try { 82 | const { query, filters } = this.filterQuery(params); 83 | const { whitelist } = this.options; 84 | const { where, select, include, _helper } = (0, utils_1.buildPrismaQueryParams)({ 85 | id, query, filters, whitelist 86 | }, this.options.id); 87 | if (_helper.idQueryIsObject || _helper.queryWhereExists) { 88 | const result = yield this.Model.findFirst(Object.assign({ where: (0, utils_1.buildWhereWithOptionalIdObject)(id, where, this.options.id) }, (0, utils_1.buildSelectOrInclude)({ select, include }))); 89 | if (!result) 90 | throw new errors.NotFound(`No record found for id '${id}' and query`); 91 | return result; 92 | } 93 | (0, utils_1.checkIdInQuery)({ id, query, idField: this.options.id }); 94 | const result = yield this.Model.findUnique(Object.assign({ where }, (0, utils_1.buildSelectOrInclude)({ select, include }))); 95 | if (!result) 96 | throw new errors.NotFound(`No record found for id '${id}'`); 97 | return result; 98 | } 99 | catch (e) { 100 | (0, error_handler_1.errorHandler)(e, 'findUnique'); 101 | } 102 | }); 103 | } 104 | _create(data, params = {}) { 105 | return __awaiter(this, void 0, void 0, function* () { 106 | const { query, filters } = this.filterQuery(params); 107 | const { whitelist } = this.options; 108 | const { select, include } = (0, utils_1.buildPrismaQueryParams)({ query, filters, whitelist }, this.options.id); 109 | try { 110 | if (Array.isArray(data)) { 111 | const result = yield this.client.$transaction(data.map((d) => this.Model.create(Object.assign({ data: d }, (0, utils_1.buildSelectOrInclude)({ select, include }))))); 112 | return result; 113 | } 114 | const result = yield this.Model.create(Object.assign({ data }, (0, utils_1.buildSelectOrInclude)({ select, include }))); 115 | return result; 116 | } 117 | catch (e) { 118 | (0, error_handler_1.errorHandler)(e); 119 | } 120 | }); 121 | } 122 | _update(id, data, params = {}, returnResult = false) { 123 | return __awaiter(this, void 0, void 0, function* () { 124 | const { query, filters } = this.filterQuery(params); 125 | const { whitelist } = this.options; 126 | const { where, select, include, _helper } = (0, utils_1.buildPrismaQueryParams)({ 127 | id, query, filters, whitelist, 128 | }, this.options.id); 129 | try { 130 | if (_helper.idQueryIsObject) { 131 | const newWhere = (0, utils_1.buildWhereWithOptionalIdObject)(id, where, this.options.id); 132 | const [, result] = yield this.client.$transaction([ 133 | this.Model.updateMany(Object.assign({ data, where: newWhere }, (0, utils_1.buildSelectOrInclude)({ select, include }))), 134 | this.Model.findFirst(Object.assign({ where: Object.assign(Object.assign({}, newWhere), data) }, (0, utils_1.buildSelectOrInclude)({ select, include }))), 135 | ]); 136 | if (!result) 137 | throw new errors.NotFound(`No record found for id '${id}'`); 138 | return result; 139 | } 140 | (0, utils_1.checkIdInQuery)({ id, query, idField: this.options.id }); 141 | const result = yield this.Model.update(Object.assign({ data, 142 | where }, (0, utils_1.buildSelectOrInclude)({ select, include }))); 143 | if (select || returnResult) { 144 | return result; 145 | } 146 | return Object.assign({ [this.options.id]: result.id }, data); 147 | } 148 | catch (e) { 149 | (0, error_handler_1.errorHandler)(e, 'update'); 150 | } 151 | }); 152 | } 153 | _patch(id, data, params = {}) { 154 | return __awaiter(this, void 0, void 0, function* () { 155 | if (id && !Array.isArray(data)) { 156 | const result = yield this._update(id, data, params, true); 157 | return result; 158 | } 159 | const { query, filters } = this.filterQuery(params); 160 | const { whitelist } = this.options; 161 | const { where, select, include } = (0, utils_1.buildPrismaQueryParams)({ query, filters, whitelist }, this.options.id); 162 | try { 163 | const [, result] = yield this.client.$transaction([ 164 | this.Model.updateMany(Object.assign({ data, 165 | where }, (0, utils_1.buildSelectOrInclude)({ select, include }))), 166 | this.Model.findMany(Object.assign({ where: Object.assign(Object.assign({}, where), data) }, (0, utils_1.buildSelectOrInclude)({ select, include }))), 167 | ]); 168 | return result; 169 | } 170 | catch (e) { 171 | (0, error_handler_1.errorHandler)(e, 'updateMany'); 172 | } 173 | }); 174 | } 175 | _remove(id, params = {}) { 176 | return __awaiter(this, void 0, void 0, function* () { 177 | const { query, filters } = this.filterQuery(params); 178 | const { whitelist } = this.options; 179 | const { where, select, include, _helper } = (0, utils_1.buildPrismaQueryParams)({ 180 | id: id || undefined, query, filters, whitelist, 181 | }, this.options.id); 182 | if (id && !_helper.idQueryIsObject) { 183 | try { 184 | (0, utils_1.checkIdInQuery)({ id, query, allowOneOf: true, idField: this.options.id }); 185 | const result = yield this.Model.delete(Object.assign({ where: id ? { [this.options.id]: id } : where }, (0, utils_1.buildSelectOrInclude)({ select, include }))); 186 | return result; 187 | } 188 | catch (e) { 189 | (0, error_handler_1.errorHandler)(e, 'delete'); 190 | } 191 | } 192 | try { 193 | const query = Object.assign({ where: id ? (0, utils_1.buildWhereWithOptionalIdObject)(id, where, this.options.id) : where }, (0, utils_1.buildSelectOrInclude)({ select, include })); 194 | const [data] = yield this.client.$transaction([ 195 | id ? this.Model.findFirst(query) : this.Model.findMany(query), 196 | this.Model.deleteMany(query), 197 | ]); 198 | if (id && !data) 199 | throw new errors.NotFound(`No record found for id '${id}'`); 200 | return data; 201 | } 202 | catch (e) { 203 | (0, error_handler_1.errorHandler)(e, 'deleteMany'); 204 | } 205 | }); 206 | } 207 | } 208 | exports.PrismaService = PrismaService; 209 | function service(options, client) { 210 | return new PrismaService(options, client); 211 | } 212 | exports.service = service; 213 | exports.prismaService = service; 214 | -------------------------------------------------------------------------------- /dist/types.d.ts: -------------------------------------------------------------------------------- 1 | export declare type PrismaClient = any; 2 | export declare type IdField = string | number; 3 | export declare type Paginate = { 4 | default?: number; 5 | max?: number; 6 | }; 7 | export interface PrismaServiceOptions { 8 | model: string; 9 | events?: string[]; 10 | multi?: boolean | string[]; 11 | id?: string; 12 | paginate?: Paginate; 13 | whitelist?: string[]; 14 | filters?: string[]; 15 | } 16 | export declare type EagerQuery = (string | string[] | string[][])[] | Record; 17 | export interface QueryParamRecordFilters { 18 | $in?: (string | boolean | number)[]; 19 | $nin?: (string | boolean | number)[]; 20 | $lt?: string | number; 21 | $lte?: string | number; 22 | $gt?: string | number; 23 | $gte?: string | number; 24 | $ne?: string | boolean | number; 25 | $eager?: EagerQuery; 26 | /** 27 | * @deprecated use $prisma instead 28 | */ 29 | $rawWhere?: Record; 30 | $prisma?: Record; 31 | $contains?: string; 32 | $search?: string; 33 | $startsWith?: string; 34 | $endsWith?: string; 35 | $mode?: string; 36 | } 37 | export declare type QueryParamRecord = string | boolean | number; 38 | export declare type QueryParamRecordsOr = Record[]; 39 | export declare type QueryParam = { 40 | [key: string]: string | boolean | number | QueryParamRecordFilters | QueryParamRecordsOr; 41 | }; 42 | -------------------------------------------------------------------------------- /dist/types.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /dist/utils.d.ts: -------------------------------------------------------------------------------- 1 | import { NullableId } from '@feathersjs/feathers'; 2 | import { EagerQuery, IdField, QueryParam, QueryParamRecordFilters } from './types'; 3 | export declare const castToNumberBooleanStringOrNull: (value: string | boolean | number) => string | number | boolean | null; 4 | export declare const castFeathersQueryToPrismaFilters: (p: QueryParamRecordFilters, whitelist: string[]) => Record; 5 | export declare const castEagerQueryToPrismaInclude: (value: EagerQuery, whitelist: string[], idField: string) => Record; 6 | export declare const mergeFiltersWithSameKey: (where: Record, key: string, filter: Record | string | number | boolean | null) => Record | string | number | boolean; 7 | export declare const buildWhereAndInclude: (query: QueryParam, whitelist: string[], idField: string) => { 8 | where: Record; 9 | include: Record; 10 | }; 11 | export declare const buildSelect: ($select: string[]) => Record; 12 | export declare const buildOrderBy: ($sort: Record) => { 13 | [x: string]: string; 14 | }[]; 15 | export declare const buildPagination: ($skip: number, $limit: number) => { 16 | skip: number; 17 | take: number; 18 | }; 19 | export declare const buildPrismaQueryParams: ({ id, query, filters, whitelist }: { 20 | id?: IdField | undefined; 21 | query: Record; 22 | filters: Record; 23 | whitelist: string[]; 24 | }, idField: string) => { 25 | skip: number; 26 | take: number; 27 | orderBy: { 28 | [x: string]: string; 29 | }[]; 30 | where: Record; 31 | select: Record; 32 | _helper: { 33 | queryWhereExists: boolean; 34 | idQueryIsObject: boolean; 35 | }; 36 | include?: undefined; 37 | } | { 38 | skip: number; 39 | take: number; 40 | orderBy: { 41 | [x: string]: string; 42 | }[]; 43 | where: Record; 44 | include: Record; 45 | _helper: { 46 | queryWhereExists: boolean; 47 | idQueryIsObject: boolean; 48 | }; 49 | select?: undefined; 50 | } | { 51 | skip: number; 52 | take: number; 53 | orderBy: { 54 | [x: string]: string; 55 | }[]; 56 | where: Record; 57 | _helper: { 58 | queryWhereExists: boolean; 59 | idQueryIsObject: boolean; 60 | }; 61 | select?: undefined; 62 | include?: undefined; 63 | }; 64 | export declare const buildSelectOrInclude: ({ select, include }: { 65 | select?: Record | undefined; 66 | include?: Record | undefined; 67 | }) => { 68 | select: Record; 69 | include?: undefined; 70 | } | { 71 | include: Record; 72 | select?: undefined; 73 | } | { 74 | select?: undefined; 75 | include?: undefined; 76 | }; 77 | export declare const checkIdInQuery: ({ id, query, idField, allowOneOf, }: { 78 | id: IdField | null; 79 | query: Record; 80 | idField: string; 81 | allowOneOf?: boolean | undefined; 82 | }) => void; 83 | export declare const buildWhereWithOptionalIdObject: (id: NullableId, where: Record, idField: string) => Record; 84 | -------------------------------------------------------------------------------- /dist/utils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.buildWhereWithOptionalIdObject = exports.checkIdInQuery = exports.buildSelectOrInclude = exports.buildPrismaQueryParams = exports.buildPagination = exports.buildOrderBy = exports.buildSelect = exports.buildWhereAndInclude = exports.mergeFiltersWithSameKey = exports.castEagerQueryToPrismaInclude = exports.castFeathersQueryToPrismaFilters = exports.castToNumberBooleanStringOrNull = void 0; 4 | const errors_1 = require("@feathersjs/errors"); 5 | const constants_1 = require("./constants"); 6 | const castToNumberBooleanStringOrNull = (value) => { 7 | const isNumber = typeof value === 'number'; 8 | const isBoolean = value === 'true' || value === 'false'; 9 | if (isBoolean || typeof value === 'boolean') { 10 | return typeof value === 'string' ? value === 'true' : value; 11 | } 12 | if (isNumber) { 13 | return value; 14 | } 15 | if (value === 'null') { 16 | return null; 17 | } 18 | return value; 19 | }; 20 | exports.castToNumberBooleanStringOrNull = castToNumberBooleanStringOrNull; 21 | const castFeathersQueryToPrismaFilters = (p, whitelist) => { 22 | const filters = {}; 23 | Object.keys(p).forEach((k) => { 24 | const key = k; 25 | const prismaKey = constants_1.OPERATORS_MAP[key]; 26 | if (prismaKey && key !== '$eager' && whitelist.includes(key)) { 27 | const value = p[key]; 28 | if (Array.isArray(value)) { 29 | filters[prismaKey] = value.map((v) => (0, exports.castToNumberBooleanStringOrNull)(v)); 30 | } 31 | else if (key === '$rawWhere' && typeof value === 'object' || key === '$prisma' && typeof value === 'object') { 32 | Object.keys(value).forEach((rawKey) => { 33 | filters[rawKey] = value[rawKey]; 34 | }); 35 | } 36 | else if (value !== undefined && typeof value !== 'object') { 37 | filters[prismaKey] = (0, exports.castToNumberBooleanStringOrNull)(value); 38 | } 39 | } 40 | }); 41 | return filters; 42 | }; 43 | exports.castFeathersQueryToPrismaFilters = castFeathersQueryToPrismaFilters; 44 | const castEagerQueryToPrismaInclude = (value, whitelist, idField) => { 45 | const include = {}; 46 | if (Array.isArray(value)) { 47 | value.forEach((v) => { 48 | if (Array.isArray(v) && typeof v[0] === 'string' && v.length > 1) { 49 | const [key, ...includes] = v; 50 | const subinclude = (0, exports.castEagerQueryToPrismaInclude)(includes, whitelist, idField); 51 | include[key] = { 52 | include: subinclude, 53 | }; 54 | } 55 | else if (Array.isArray(v) && typeof v[0] === 'string' && v.length === 1) { 56 | const [key] = v; 57 | include[key] = true; 58 | } 59 | else if (typeof v[0] !== 'string') { 60 | throw { 61 | code: 'FP1001', 62 | message: 'First Array Item in a sub-array must be a string!', 63 | }; 64 | } 65 | else if (typeof v === 'string') { 66 | include[v] = true; 67 | } 68 | }); 69 | } 70 | else { 71 | Object.keys(value).forEach((key) => { 72 | const val = value[key]; 73 | if (typeof val === 'boolean') { 74 | include[key] = val; 75 | } 76 | else if (Array.isArray(val)) { 77 | include[key] = { 78 | select: Object.assign({ [idField]: true }, (0, exports.buildSelect)(val)), 79 | }; 80 | } 81 | }); 82 | } 83 | return include; 84 | }; 85 | exports.castEagerQueryToPrismaInclude = castEagerQueryToPrismaInclude; 86 | const mergeFiltersWithSameKey = (where, key, filter) => { 87 | const current = where[key]; 88 | if (typeof filter === 'object') { 89 | const currentIsObj = typeof current === 'object'; 90 | return Object.assign(Object.assign(Object.assign({}, (currentIsObj ? current : {})), filter), (!currentIsObj && current ? { equals: current } : {})); 91 | } 92 | return filter; 93 | }; 94 | exports.mergeFiltersWithSameKey = mergeFiltersWithSameKey; 95 | const buildWhereAndInclude = (query, whitelist, idField) => { 96 | const where = {}; 97 | let include = {}; 98 | Object.keys(query).forEach((k) => { 99 | const value = query[k]; 100 | if (k === '$or' && Array.isArray(value)) { 101 | where.OR = value.map((v) => (0, exports.buildWhereAndInclude)(v, whitelist, idField).where); 102 | } 103 | else if (k === '$and' && Array.isArray(value)) { 104 | value.forEach((v) => { 105 | const whereValue = (0, exports.buildWhereAndInclude)(v, whitelist, idField).where; 106 | Object.keys(whereValue).map((subKey) => { 107 | where[subKey] = (0, exports.mergeFiltersWithSameKey)(where, subKey, whereValue[subKey]); 108 | }); 109 | }); 110 | } 111 | else if (k !== '$eager' && typeof value === 'object' && !Array.isArray(value)) { 112 | where[k] = (0, exports.mergeFiltersWithSameKey)(where, k, (0, exports.castFeathersQueryToPrismaFilters)(value, whitelist)); 113 | } 114 | else if (k !== '$eager' && typeof value !== 'object' && !Array.isArray(value)) { 115 | where[k] = (0, exports.castToNumberBooleanStringOrNull)(value); 116 | } 117 | else if (k === '$eager' && whitelist.includes(k)) { 118 | const eager = value; 119 | include = (0, exports.castEagerQueryToPrismaInclude)(eager, whitelist, idField); 120 | } 121 | }); 122 | return { where, include }; 123 | }; 124 | exports.buildWhereAndInclude = buildWhereAndInclude; 125 | const buildSelect = ($select) => { 126 | const select = {}; 127 | $select.forEach((f) => { select[f] = true; }); 128 | return select; 129 | }; 130 | exports.buildSelect = buildSelect; 131 | const buildOrderBy = ($sort) => { 132 | return Object.keys($sort).map((k) => ({ [k]: $sort[k] === 1 ? 'asc' : 'desc' })); 133 | }; 134 | exports.buildOrderBy = buildOrderBy; 135 | const buildPagination = ($skip, $limit) => { 136 | return { 137 | skip: $skip || 0, 138 | take: $limit, 139 | }; 140 | }; 141 | exports.buildPagination = buildPagination; 142 | const buildPrismaQueryParams = ({ id, query, filters, whitelist }, idField) => { 143 | let select = (0, exports.buildSelect)(filters.$select || []); 144 | const selectExists = Object.keys(select).length > 0; 145 | const { where, include } = (0, exports.buildWhereAndInclude)(id ? Object.assign({ [idField]: id }, query) : query, whitelist, idField); 146 | const includeExists = Object.keys(include).length > 0; 147 | const orderBy = (0, exports.buildOrderBy)(filters.$sort || {}); 148 | const { skip, take } = (0, exports.buildPagination)(filters.$skip, filters.$limit); 149 | const queryWhereExists = Object.keys(where).filter((k) => k !== idField).length > 0; 150 | const idQueryIsObject = typeof where.id === 'object'; 151 | if (selectExists) { 152 | select = Object.assign(Object.assign({ [idField]: true }, select), include); 153 | return { 154 | skip, 155 | take, 156 | orderBy, 157 | where, 158 | select, 159 | _helper: { 160 | queryWhereExists, 161 | idQueryIsObject 162 | }, 163 | }; 164 | } 165 | if (!selectExists && includeExists) { 166 | return { 167 | skip, 168 | take, 169 | orderBy, 170 | where, 171 | include, 172 | _helper: { 173 | queryWhereExists, 174 | idQueryIsObject 175 | }, 176 | }; 177 | } 178 | return { 179 | skip, 180 | take, 181 | orderBy, 182 | where, 183 | _helper: { 184 | queryWhereExists, 185 | idQueryIsObject 186 | }, 187 | }; 188 | }; 189 | exports.buildPrismaQueryParams = buildPrismaQueryParams; 190 | const buildSelectOrInclude = ({ select, include }) => { 191 | return select ? { select } : include ? { include } : {}; 192 | }; 193 | exports.buildSelectOrInclude = buildSelectOrInclude; 194 | const checkIdInQuery = ({ id, query, idField, allowOneOf, }) => { 195 | if ((allowOneOf && id && Object.keys(query).length > 0) || (id && query[idField] && id !== query[idField])) { 196 | throw new errors_1.NotFound(`No record found for ${idField} '${id}' and query.${idField} '${id}'`); 197 | } 198 | }; 199 | exports.checkIdInQuery = checkIdInQuery; 200 | const buildWhereWithOptionalIdObject = (id, where, idField) => { 201 | if (typeof where.id === 'object') { 202 | return Object.assign(Object.assign({}, where), { [idField]: Object.assign(Object.assign({}, where[idField]), { equals: id }) }); 203 | } 204 | return where; 205 | }; 206 | exports.buildWhereWithOptionalIdObject = buildWhereWithOptionalIdObject; 207 | -------------------------------------------------------------------------------- /mocha.opts: -------------------------------------------------------------------------------- 1 | --recursive test/ 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "feathers-prisma", 3 | "description": "A Feathers service adapter for Prisma ORM.", 4 | "version": "0.7.0", 5 | "homepage": "https://github.com/ps73/feathers-prisma", 6 | "main": "dist/", 7 | "types": "dist/", 8 | "files": [ 9 | "dist" 10 | ], 11 | "keywords": [ 12 | "feathers", 13 | "prisma", 14 | "feathers-plugin", 15 | "postgresql", 16 | "mariadb", 17 | "mysql", 18 | "mssql", 19 | "sqlite", 20 | "aurora", 21 | "azure-sql" 22 | ], 23 | "license": "MIT", 24 | "repository": { 25 | "type": "git", 26 | "url": "git://github.com/ps73/feathers-prisma.git" 27 | }, 28 | "author": { 29 | "name": "ps73", 30 | "email": "ps7330e@gmail.com", 31 | "url": "https://github.com/ps73" 32 | }, 33 | "contributors": [], 34 | "bugs": { 35 | "url": "https://github.com/ps73/feathers-prisma/issues" 36 | }, 37 | "engines": { 38 | "node": ">= 16" 39 | }, 40 | "scripts": { 41 | "generate": "prisma generate", 42 | "compile": "tsc", 43 | "dev": "./node_modules/nodemon/bin/nodemon.js -e ts --watch src --exec \"npm run compile\"", 44 | "publish": "git push origin --tags && npm run compile && git push origin", 45 | "release:pre": "npm run compile && npm version prerelease && npm publish --tag pre", 46 | "release:patch": "npm run compile && npm version patch && npm publish", 47 | "release:minor": "npm run compile && npm version minor && npm publish", 48 | "release:major": "npm run compile && npm version major && npm publish", 49 | "changelog": "github_changelog_generator && git add CHANGELOG.md && git commit -am \"Updating changelog\"", 50 | "lint": "eslint src/*.ts src/**/*.ts test/*.js test/**/*.js --fix", 51 | "mocha": "mocha", 52 | "coverage": "istanbul cover node_modules/mocha/bin/_mocha -- --config=.mocharc.json", 53 | "test": "npm run compile && npm run lint -- --cache && npm run coverage" 54 | }, 55 | "semistandard": { 56 | "env": [ 57 | "mocha" 58 | ] 59 | }, 60 | "directories": { 61 | "lib": "lib" 62 | }, 63 | "peerDependencies": { 64 | "@feathersjs/adapter-commons": "4.*", 65 | "@feathersjs/feathers": "4.*", 66 | "@feathersjs/errors": "4.*", 67 | "@prisma/client": "5.*" 68 | }, 69 | "devDependencies": { 70 | "@feathersjs/adapter-commons": "^4.5.17", 71 | "@feathersjs/errors": "^4.5.17", 72 | "@feathersjs/adapter-tests": "^4.5.16", 73 | "@feathersjs/express": "^4.5.18", 74 | "@feathersjs/feathers": "^4.5.17", 75 | "@prisma/client": "^5.9.1", 76 | "@types/node": "^17.0.5", 77 | "@typescript-eslint/eslint-plugin": "^5.8.1", 78 | "@typescript-eslint/parser": "^5.8.1", 79 | "chai": "^3.5.0", 80 | "eslint": "^7.32.0", 81 | "eslint-config-airbnb-base": "^15.0.0", 82 | "eslint-plugin-import": "^2.25.3", 83 | "istanbul": "^1.1.0-alpha.1", 84 | "mocha": "^9.1.3", 85 | "nodemon": "^2.0.15", 86 | "prisma": "^5.9.1", 87 | "semistandard": "^16.0.1", 88 | "typescript": "^4.5.4" 89 | }, 90 | "prisma": { 91 | "schema": "./prisma/schema.prisma" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /prisma/migrations/20220107090636_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "User" ( 3 | "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 4 | "name" TEXT, 5 | "age" INTEGER, 6 | "time" INTEGER, 7 | "created" BOOLEAN 8 | ); 9 | 10 | -- CreateTable 11 | CREATE TABLE "Todo" ( 12 | "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 13 | "title" TEXT NOT NULL, 14 | "tag1" TEXT, 15 | "tag2" TEXT, 16 | "done" BOOLEAN NOT NULL DEFAULT false, 17 | "prio" INTEGER NOT NULL DEFAULT 0, 18 | "userId" INTEGER NOT NULL, 19 | CONSTRAINT "Todo_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE 20 | ); 21 | 22 | -- CreateTable 23 | CREATE TABLE "PeopleId" ( 24 | "customid" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 25 | "name" TEXT, 26 | "age" INTEGER, 27 | "time" INTEGER, 28 | "created" BOOLEAN 29 | ); 30 | 31 | -- CreateTable 32 | CREATE TABLE "People" ( 33 | "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 34 | "name" TEXT NOT NULL, 35 | "age" INTEGER, 36 | "time" INTEGER, 37 | "created" BOOLEAN 38 | ); 39 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "sqlite" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | binaryTargets = ["native", "debian-openssl-3.0.x"] 4 | } 5 | 6 | datasource db { 7 | provider = "sqlite" 8 | url = "file:./tests.db" 9 | } 10 | 11 | model User { 12 | id Int @id @default(autoincrement()) 13 | name String? 14 | age Int? 15 | time Int? 16 | created Boolean? 17 | todos Todo[] 18 | } 19 | 20 | model Todo { 21 | id Int @id @default(autoincrement()) 22 | title String 23 | tag1 String? 24 | tag2 String? 25 | done Boolean @default(false) 26 | prio Int @default(0) 27 | userId Int 28 | user User @relation(fields: [userId], references: [id]) 29 | } 30 | 31 | model PeopleId { 32 | customid Int @id @default(autoincrement()) 33 | name String? 34 | age Int? 35 | time Int? 36 | created Boolean? 37 | } 38 | 39 | model People { 40 | id Int @id @default(autoincrement()) 41 | name String 42 | age Int? 43 | time Int? 44 | created Boolean? 45 | } 46 | -------------------------------------------------------------------------------- /prisma/tests.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ps73/feathers-prisma/16fccf348c8983da0ddca3242faa0d4e2c3846fd/prisma/tests.db -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const OPERATORS = { 2 | not: '$ne', 3 | gte: '$gte', 4 | gt: '$gt', 5 | lte: '$lte', 6 | lt: '$lt', 7 | in: '$in', 8 | notIn: '$nin', 9 | and: '$and', 10 | or: '$or', 11 | // specific to prisma 12 | contains: '$contains', 13 | startsWith: '$startsWith', 14 | endsWith: '$endsWith', 15 | mode: '$mode', 16 | }; 17 | 18 | export const OPERATORS_MAP = { 19 | $lt: 'lt', 20 | $lte: 'lte', 21 | $gt: 'gt', 22 | $gte: 'gte', 23 | $in: 'in', 24 | $nin: 'notIn', 25 | $ne: 'not', 26 | $eager: 'includes', 27 | // specific to prisma 28 | /** 29 | * @deprecated use $prisma instead 30 | */ 31 | $rawWhere: 'rawWhere', 32 | $prisma: 'prisma', 33 | $contains: 'contains', 34 | $search: 'search', 35 | $startsWith: 'startsWith', 36 | $endsWith: 'endsWith', 37 | $mode: 'mode', 38 | }; 39 | -------------------------------------------------------------------------------- /src/error-handler.ts: -------------------------------------------------------------------------------- 1 | import errors = require('@feathersjs/errors'); 2 | import { PrismaClientKnownRequestError, PrismaClientValidationError } from '@prisma/client/runtime/library'; 3 | 4 | function getType(v: number): string { 5 | let type = ''; 6 | const cases = { 7 | common: v >= 1000 && v < 2000, 8 | query: v >= 2000 && v < 3000, 9 | migration: v >= 3000 && v < 4000, 10 | introspection: v >= 4000 && v < 5000, 11 | }; 12 | Object.keys(cases).map((key) => { 13 | // @ts-ignore 14 | if (cases[key]) { 15 | type = key; 16 | } 17 | return key; 18 | }); 19 | return type; 20 | } 21 | 22 | export function errorHandler(error: any, prismaMethod?: string) { 23 | let feathersError; 24 | if (error instanceof errors.FeathersError) { 25 | feathersError = error; 26 | } else if (error instanceof PrismaClientKnownRequestError) { 27 | const { 28 | code, meta, message, clientVersion, 29 | } = error; 30 | const errType = getType(Number(code.substring(1))); 31 | switch (errType) { 32 | case 'common': 33 | feathersError = new errors.GeneralError(message, { code, meta, clientVersion }); 34 | break; 35 | case 'query': 36 | feathersError = new errors.BadRequest(message, { code, meta, clientVersion }); 37 | if (code === 'P2025') { 38 | // @ts-ignore 39 | feathersError = new errors.NotFound(meta?.cause || 'Record not found.'); 40 | } 41 | break; 42 | case 'migration': 43 | feathersError = new errors.GeneralError(message, { code, meta, clientVersion }); 44 | break; 45 | case 'introspection': 46 | feathersError = new errors.GeneralError(message, { code, meta, clientVersion }); 47 | break; 48 | default: 49 | feathersError = new errors.BadRequest(message, { code, meta, clientVersion }); 50 | break; 51 | } 52 | } else if (error instanceof PrismaClientValidationError) { 53 | switch (prismaMethod) { 54 | case 'findUnique': 55 | case 'remove': 56 | case 'update': 57 | case 'delete': 58 | feathersError = new errors.NotFound('Record not found.'); 59 | break; 60 | default: 61 | feathersError = new errors.GeneralError(error); 62 | break; 63 | } 64 | } else { 65 | feathersError = new errors.GeneralError(error); 66 | } 67 | 68 | throw feathersError; 69 | } 70 | 71 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { service, prismaService, PrismaService } from './service'; 2 | 3 | export * from './types'; 4 | export * from './constants'; 5 | export * from './error-handler'; 6 | -------------------------------------------------------------------------------- /src/service.ts: -------------------------------------------------------------------------------- 1 | import type { Params } from '@feathersjs/feathers'; 2 | import { AdapterService } from '@feathersjs/adapter-commons'; 3 | import * as errors from '@feathersjs/errors'; 4 | import { PrismaClient } from '@prisma/client'; 5 | import { IdField, PrismaServiceOptions } from './types'; 6 | import { buildPrismaQueryParams, buildSelectOrInclude, buildWhereWithOptionalIdObject, checkIdInQuery } from './utils'; 7 | import { OPERATORS } from './constants'; 8 | import { errorHandler } from './error-handler'; 9 | 10 | export class PrismaService> extends AdapterService { 11 | Model: any; 12 | client: PrismaClient; 13 | 14 | constructor(options: PrismaServiceOptions, client: PrismaClient) { 15 | super({ 16 | id: options.id || 'id', 17 | paginate: { 18 | default: options.paginate?.default, 19 | max: options.paginate?.max, 20 | }, 21 | multi: options.multi || [], 22 | filters: options.filters || [], 23 | events: options.events || [], 24 | whitelist: Object.values(OPERATORS).concat(options.whitelist || []), 25 | }); 26 | 27 | const { model } = options; 28 | if (!model) { 29 | throw new errors.GeneralError('You must provide a model string.'); 30 | } 31 | // @ts-ignore 32 | if (!client[model]) { 33 | throw new errors.GeneralError(`No model with name ${model} found in prisma client.`); 34 | } 35 | this.client = client; 36 | // @ts-ignore 37 | this.Model = client[model]; 38 | } 39 | 40 | async _find(params: Params = {}): Promise { 41 | const { query, filters } = this.filterQuery(params); 42 | const { whitelist } = this.options; 43 | const { skip, take, orderBy, where, select, include } = buildPrismaQueryParams({ 44 | query, filters, whitelist, 45 | }, this.options.id); 46 | try { 47 | const findMany = () => { 48 | return this.Model.findMany({ 49 | ...(typeof take === 'number' ? { skip, take } : { skip }), 50 | orderBy, 51 | where, 52 | ...buildSelectOrInclude({ select, include }), 53 | }); 54 | }; 55 | 56 | if (!this.options.paginate.default || (typeof take !== 'number' && !take)) { 57 | const data = await findMany(); 58 | return data; 59 | } 60 | 61 | const [data, count] = await this.client.$transaction([ 62 | findMany(), 63 | this.Model.count({ 64 | where, 65 | }), 66 | ]); 67 | 68 | const result = { 69 | total: count, 70 | skip, 71 | limit: take, 72 | data, 73 | }; 74 | return result; 75 | } catch (e) { 76 | errorHandler(e); 77 | } 78 | } 79 | 80 | async _get(id: IdField, params: Params = {}) { 81 | try { 82 | const { query, filters } = this.filterQuery(params); 83 | const { whitelist } = this.options; 84 | const { where, select, include, _helper } = buildPrismaQueryParams({ 85 | id, query, filters, whitelist 86 | }, this.options.id); 87 | if (_helper.idQueryIsObject || _helper.queryWhereExists) { 88 | const result: Partial = await this.Model.findFirst({ 89 | where: buildWhereWithOptionalIdObject(id, where, this.options.id), 90 | ...buildSelectOrInclude({ select, include }), 91 | }); 92 | if (!result) throw new errors.NotFound(`No record found for id '${id}' and query`); 93 | return result; 94 | } 95 | checkIdInQuery({ id, query, idField: this.options.id }); 96 | const result: Partial = await this.Model.findUnique({ 97 | where, 98 | ...buildSelectOrInclude({ select, include }), 99 | }); 100 | if (!result) throw new errors.NotFound(`No record found for id '${id}'`); 101 | return result; 102 | } catch (e) { 103 | errorHandler(e, 'findUnique'); 104 | } 105 | } 106 | 107 | async _create(data: Partial | Partial[], params: Params = {}) { 108 | const { query, filters } = this.filterQuery(params); 109 | const { whitelist } = this.options; 110 | const { select, include } = buildPrismaQueryParams({ query, filters, whitelist }, this.options.id); 111 | try { 112 | if (Array.isArray(data)) { 113 | const result: Partial[] = await this.client.$transaction(data.map((d) => this.Model.create({ 114 | data: d, 115 | ...buildSelectOrInclude({ select, include }), 116 | }))); 117 | return result; 118 | } 119 | const result: Partial = await this.Model.create({ 120 | data, 121 | ...buildSelectOrInclude({ select, include }), 122 | }); 123 | return result; 124 | } catch (e) { 125 | errorHandler(e); 126 | } 127 | } 128 | 129 | async _update(id: IdField, data: Partial, params: Params = {}, returnResult = false) { 130 | const { query, filters } = this.filterQuery(params); 131 | const { whitelist } = this.options; 132 | const { where, select, include, _helper } = buildPrismaQueryParams({ 133 | id, query, filters, whitelist, 134 | }, this.options.id); 135 | try { 136 | if (_helper.idQueryIsObject) { 137 | const newWhere = buildWhereWithOptionalIdObject(id, where, this.options.id); 138 | const [, result] = await this.client.$transaction([ 139 | this.Model.updateMany({ 140 | data, 141 | where: newWhere, 142 | ...buildSelectOrInclude({ select, include }), 143 | }), 144 | this.Model.findFirst({ 145 | where: { 146 | ...newWhere, 147 | ...data, 148 | }, 149 | ...buildSelectOrInclude({ select, include }), 150 | }), 151 | ]); 152 | if (!result) throw new errors.NotFound(`No record found for id '${id}'`); 153 | return result; 154 | } 155 | checkIdInQuery({ id, query, idField: this.options.id }); 156 | const result = await this.Model.update({ 157 | data, 158 | where, 159 | ...buildSelectOrInclude({ select, include }), 160 | }); 161 | if (select || returnResult) { 162 | return result; 163 | } 164 | return { [this.options.id]: result.id, ...data }; 165 | } catch (e) { 166 | errorHandler(e, 'update'); 167 | } 168 | } 169 | 170 | async _patch(id: IdField | null, data: Partial | Partial[], params: Params = {}) { 171 | if (id && !Array.isArray(data)) { 172 | const result = await this._update(id, data, params, true); 173 | return result; 174 | } 175 | const { query, filters } = this.filterQuery(params); 176 | const { whitelist } = this.options; 177 | const { where, select, include } = buildPrismaQueryParams({ query, filters, whitelist }, this.options.id); 178 | try { 179 | const [, result] = await this.client.$transaction([ 180 | this.Model.updateMany({ 181 | data, 182 | where, 183 | ...buildSelectOrInclude({ select, include }), 184 | }), 185 | this.Model.findMany({ 186 | where: { 187 | ...where, 188 | ...data, 189 | }, 190 | ...buildSelectOrInclude({ select, include }), 191 | }), 192 | ]); 193 | return result; 194 | } catch (e) { 195 | errorHandler(e, 'updateMany'); 196 | } 197 | } 198 | 199 | async _remove(id: IdField | null, params: Params = {}) { 200 | const { query, filters } = this.filterQuery(params); 201 | const { whitelist } = this.options; 202 | const { where, select, include, _helper } = buildPrismaQueryParams({ 203 | id: id || undefined, query, filters, whitelist, 204 | }, this.options.id); 205 | if (id && !_helper.idQueryIsObject) { 206 | try { 207 | checkIdInQuery({ id, query, allowOneOf: true, idField: this.options.id }); 208 | const result: Partial = await this.Model.delete({ 209 | where: id ? { [this.options.id]: id } : where, 210 | ...buildSelectOrInclude({ select, include }), 211 | }); 212 | return result; 213 | } catch (e) { 214 | errorHandler(e, 'delete'); 215 | } 216 | } 217 | try { 218 | const query = { 219 | where: id ? buildWhereWithOptionalIdObject(id, where, this.options.id) : where, 220 | ...buildSelectOrInclude({ select, include }), 221 | }; 222 | const [data] = await this.client.$transaction([ 223 | id ? this.Model.findFirst(query) : this.Model.findMany(query), 224 | this.Model.deleteMany(query), 225 | ]); 226 | if (id && !data) throw new errors.NotFound(`No record found for id '${id}'`); 227 | return data; 228 | } catch (e) { 229 | errorHandler(e, 'deleteMany'); 230 | } 231 | } 232 | } 233 | 234 | export function service>(options: PrismaServiceOptions, client: PrismaClient) { 235 | return new PrismaService(options, client); 236 | } 237 | 238 | export const prismaService = service; 239 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type PrismaClient = any; 2 | 3 | export type IdField = string | number; 4 | 5 | export type Paginate = { 6 | default?: number; 7 | max?: number; 8 | } 9 | 10 | export interface PrismaServiceOptions { 11 | model: string; 12 | events?: string[]; 13 | multi?: boolean | string[]; 14 | id?: string; 15 | paginate?: Paginate; 16 | whitelist?: string[]; 17 | filters?: string[]; 18 | } 19 | 20 | export type EagerQuery = (string | string[] | string[][])[] | Record; 21 | 22 | export interface QueryParamRecordFilters { 23 | $in?: (string | boolean | number)[]; 24 | $nin?: (string | boolean | number)[]; 25 | $lt?: string | number; 26 | $lte?: string | number; 27 | $gt?: string | number; 28 | $gte?: string | number; 29 | $ne?: string | boolean | number; 30 | $eager?: EagerQuery; 31 | // prisma specific 32 | /** 33 | * @deprecated use $prisma instead 34 | */ 35 | $rawWhere?: Record; 36 | $prisma?: Record; 37 | $contains?: string; 38 | $search?: string; 39 | $startsWith?: string; 40 | $endsWith?: string; 41 | $mode?: string; 42 | } 43 | 44 | export type QueryParamRecord = string | boolean | number; 45 | export type QueryParamRecordsOr = Record[]; 46 | 47 | export type QueryParam = { 48 | [key: string]: string | boolean | number | QueryParamRecordFilters | QueryParamRecordsOr; 49 | } 50 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { NotFound } from '@feathersjs/errors'; 2 | import { NullableId } from '@feathersjs/feathers'; 3 | import { OPERATORS_MAP } from './constants'; 4 | import { EagerQuery, IdField, QueryParam, QueryParamRecordFilters } from './types'; 5 | 6 | export const castToNumberBooleanStringOrNull = (value: string | boolean | number) => { 7 | const isNumber = typeof value === 'number'; 8 | const isBoolean = value === 'true' || value === 'false'; 9 | if (isBoolean || typeof value === 'boolean') { 10 | return typeof value === 'string' ? value === 'true' : value; 11 | } 12 | if (isNumber) { 13 | return value; 14 | } 15 | if (value === 'null') { 16 | return null; 17 | } 18 | return value; 19 | }; 20 | 21 | export const castFeathersQueryToPrismaFilters = (p: QueryParamRecordFilters, whitelist: string[]) => { 22 | const filters: Record = {}; 23 | Object.keys(p).forEach((k: string) => { 24 | const key = (k as keyof typeof OPERATORS_MAP); 25 | const prismaKey = OPERATORS_MAP[key]; 26 | if (prismaKey && key !== '$eager' && whitelist.includes(key)) { 27 | const value = p[key]; 28 | if (Array.isArray(value)) { 29 | filters[prismaKey] = value.map((v) => castToNumberBooleanStringOrNull(v)); 30 | } else if (key === '$rawWhere' && typeof value === 'object' || key === '$prisma' && typeof value === 'object') { 31 | Object.keys(value).forEach((rawKey) => { 32 | filters[rawKey] = value[rawKey]; 33 | }); 34 | } else if (value !== undefined && typeof value !== 'object') { 35 | filters[prismaKey] = castToNumberBooleanStringOrNull(value); 36 | } 37 | } 38 | }); 39 | return filters; 40 | }; 41 | 42 | export const castEagerQueryToPrismaInclude = (value: EagerQuery, whitelist: string[], idField: string) => { 43 | const include: Record = {}; 44 | if (Array.isArray(value)) { 45 | value.forEach((v) => { 46 | if (Array.isArray(v) && typeof v[0] === 'string' && v.length > 1) { 47 | const [key, ...includes] = v; 48 | const subinclude = castEagerQueryToPrismaInclude(includes, whitelist, idField); 49 | include[key] = { 50 | include: subinclude, 51 | }; 52 | } else if (Array.isArray(v) && typeof v[0] === 'string' && v.length === 1) { 53 | const [key] = v; 54 | include[key] = true; 55 | } else if (typeof v[0] !== 'string') { 56 | throw { 57 | code: 'FP1001', 58 | message: 'First Array Item in a sub-array must be a string!', 59 | }; 60 | } else if (typeof v === 'string') { 61 | include[v] = true; 62 | } 63 | }); 64 | } else { 65 | Object.keys(value).forEach((key) => { 66 | const val = value[key]; 67 | if (typeof val === 'boolean') { 68 | include[key] = val; 69 | } else if (Array.isArray(val)) { 70 | include[key] = { 71 | select: { 72 | [idField]: true, 73 | ...buildSelect(val), 74 | }, 75 | }; 76 | } 77 | }); 78 | } 79 | 80 | return include; 81 | }; 82 | 83 | export const mergeFiltersWithSameKey = ( 84 | where: Record, 85 | key: string, 86 | filter: Record | string | number | boolean | null, 87 | ): Record | string | number | boolean => { 88 | const current = where[key]; 89 | if (typeof filter === 'object') { 90 | const currentIsObj = typeof current === 'object'; 91 | return { 92 | ...(currentIsObj ? current : {}), 93 | ...filter, 94 | ...(!currentIsObj && current ? { equals: current } : {}) 95 | }; 96 | } 97 | return filter; 98 | }; 99 | 100 | export const buildWhereAndInclude = (query: QueryParam, whitelist: string[], idField: string) => { 101 | const where: Record = {}; 102 | let include: Record = {}; 103 | Object.keys(query).forEach((k: string | '$or' | '$and') => { 104 | const value = query[k]; 105 | if (k === '$or' && Array.isArray(value)) { 106 | where.OR = value.map((v) => buildWhereAndInclude(v, whitelist, idField).where); 107 | } else if (k === '$and' && Array.isArray(value)) { 108 | value.forEach((v) => { 109 | const whereValue = buildWhereAndInclude(v, whitelist, idField).where; 110 | Object.keys(whereValue).map((subKey) => { 111 | where[subKey] = mergeFiltersWithSameKey(where, subKey, whereValue[subKey]); 112 | }); 113 | }); 114 | } else if (k !== '$eager' && typeof value === 'object' && !Array.isArray(value)) { 115 | where[k] = mergeFiltersWithSameKey(where, k, castFeathersQueryToPrismaFilters(value, whitelist)); 116 | } else if (k !== '$eager' && typeof value !== 'object' && !Array.isArray(value)) { 117 | where[k] = castToNumberBooleanStringOrNull(value); 118 | } else if (k === '$eager' && whitelist.includes(k)) { 119 | const eager = value as EagerQuery; 120 | include = castEagerQueryToPrismaInclude(eager, whitelist, idField); 121 | } 122 | }); 123 | return { where, include }; 124 | }; 125 | 126 | export const buildSelect = ($select: string[]) => { 127 | const select: Record = {}; 128 | $select.forEach((f: string) => { select[f] = true; }); 129 | return select; 130 | }; 131 | 132 | export const buildOrderBy = ($sort: Record) => { 133 | return Object.keys($sort).map((k) => ({ [k]: $sort[k] === 1 ? 'asc' : 'desc' })); 134 | }; 135 | 136 | export const buildPagination = ($skip: number, $limit: number) => { 137 | return { 138 | skip: $skip || 0, 139 | take: $limit, 140 | }; 141 | }; 142 | 143 | export const buildPrismaQueryParams = ( 144 | { id, query, filters, whitelist }: { 145 | id?: IdField, 146 | query: Record, 147 | filters: Record, 148 | whitelist: string[], 149 | }, 150 | idField: string, 151 | ) => { 152 | let select = buildSelect(filters.$select || []); 153 | const selectExists = Object.keys(select).length > 0; 154 | const { where, include } = buildWhereAndInclude(id ? { [idField]: id, ...query } : query, whitelist, idField); 155 | const includeExists = Object.keys(include).length > 0; 156 | const orderBy = buildOrderBy(filters.$sort || {}); 157 | const { skip, take } = buildPagination(filters.$skip, filters.$limit); 158 | const queryWhereExists = Object.keys(where).filter((k) => k !== idField).length > 0; 159 | const idQueryIsObject = typeof where.id === 'object'; 160 | 161 | if (selectExists) { 162 | select = { 163 | [idField]: true, 164 | ...select, 165 | ...include, 166 | }; 167 | 168 | return { 169 | skip, 170 | take, 171 | orderBy, 172 | where, 173 | select, 174 | _helper: { 175 | queryWhereExists, 176 | idQueryIsObject 177 | }, 178 | }; 179 | } 180 | 181 | if (!selectExists && includeExists) { 182 | return { 183 | skip, 184 | take, 185 | orderBy, 186 | where, 187 | include, 188 | _helper: { 189 | queryWhereExists, 190 | idQueryIsObject 191 | }, 192 | }; 193 | } 194 | 195 | return { 196 | skip, 197 | take, 198 | orderBy, 199 | where, 200 | _helper: { 201 | queryWhereExists, 202 | idQueryIsObject 203 | }, 204 | }; 205 | }; 206 | 207 | export const buildSelectOrInclude = ( 208 | { select, include }: { select?: Record; include?: Record }, 209 | ) => { 210 | return select ? { select } : include ? { include } : {}; 211 | }; 212 | 213 | export const checkIdInQuery = ( 214 | { 215 | id, 216 | query, 217 | idField, 218 | allowOneOf, 219 | }: { 220 | id: IdField | null; 221 | query: Record; 222 | idField: string; 223 | allowOneOf?: boolean; 224 | } 225 | ) => { 226 | if ((allowOneOf && id && Object.keys(query).length > 0) || (id && query[idField] && id !== query[idField])) { 227 | throw new NotFound(`No record found for ${idField} '${id}' and query.${idField} '${id}'`); 228 | } 229 | }; 230 | 231 | export const buildWhereWithOptionalIdObject = (id: NullableId, where: Record, idField: string) => { 232 | if (typeof where.id === 'object') { 233 | return { 234 | ...where, 235 | [idField]: { 236 | ...where[idField], 237 | equals: id, 238 | }, 239 | }; 240 | } 241 | return where; 242 | }; 243 | -------------------------------------------------------------------------------- /test/errors.test.js: -------------------------------------------------------------------------------- 1 | const { FeathersError } = require('@feathersjs/errors'); 2 | const { errorHandler } = require('../dist'); 3 | const { assert, expect } = require('chai'); 4 | const Prisma = require('@prisma/client'); 5 | 6 | describe('the \'buildPrismaQueryParams\' function', () => { 7 | const MESSAGE = 'test'; 8 | const ERROR_BODY = {meta: 'meta', message: 'test', clientVersion: '5.9.1'}; 9 | 10 | it('It should return error if instance of errors.FeathersError', async () => { 11 | try { 12 | const error = new FeathersError('Test Error'); 13 | errorHandler(error, 'findUnique'); 14 | } catch (error) { 15 | assert.ok(error instanceof FeathersError); 16 | } 17 | }); 18 | 19 | it('It should return common type feathers error for P1000', async () => { 20 | const code = 'P1000'; 21 | const ERROR = { code, ...ERROR_BODY }; 22 | try { 23 | const error = new Prisma.PrismaClientKnownRequestError(MESSAGE, ERROR); 24 | errorHandler(error, 'findUnique'); 25 | } catch (error) { 26 | assert.ok(error instanceof FeathersError); 27 | expect(error.name).to.be.equal('GeneralError'); 28 | expect(error.message).to.be.equal(MESSAGE); 29 | expect(error.code).to.be.equal(500); 30 | expect(error.className).to.be.equal('general-error'); 31 | } 32 | }); 33 | 34 | it('It should return query type feathers error for P2025', async () => { 35 | const code = 'P2025'; 36 | const ERROR = {code, ...ERROR_BODY}; 37 | try { 38 | const error = new Prisma.PrismaClientKnownRequestError(MESSAGE, ERROR); 39 | errorHandler(error, 'findUnique'); 40 | } catch (error) { 41 | assert.ok(error instanceof FeathersError); 42 | expect(error.name).to.be.equal('NotFound'); 43 | expect(error.message).to.be.equal('Record not found.'); 44 | expect(error.code).to.be.equal(404); 45 | expect(error.className).to.be.equal('not-found'); 46 | } 47 | try { 48 | const error = new Prisma.PrismaClientKnownRequestError(MESSAGE, 49 | { 50 | ...ERROR, 51 | meta: { cause: 'Something is wrong.' }, 52 | } 53 | ); 54 | errorHandler(error, 'findUnique'); 55 | } catch (error) { 56 | assert.ok(error instanceof FeathersError); 57 | expect(error.name).to.be.equal('NotFound'); 58 | expect(error.message).to.be.equal('Something is wrong.'); 59 | expect(error.code).to.be.equal(404); 60 | expect(error.className).to.be.equal('not-found'); 61 | } 62 | try { 63 | const error = new Prisma.PrismaClientKnownRequestError(MESSAGE, { ...ERROR, meta: null }); 64 | errorHandler(error, 'findUnique'); 65 | } catch (error) { 66 | assert.ok(error instanceof FeathersError); 67 | expect(error.name).to.be.equal('NotFound'); 68 | expect(error.message).to.be.equal('Record not found.'); 69 | expect(error.code).to.be.equal(404); 70 | expect(error.className).to.be.equal('not-found'); 71 | } 72 | try { 73 | const error = new Prisma.PrismaClientKnownRequestError(MESSAGE, { code: 'P2000' }); 74 | errorHandler(error, 'findUnique'); 75 | } catch (error) { 76 | assert.ok(error instanceof FeathersError); 77 | expect(error.name).to.be.equal('BadRequest'); 78 | expect(error.message).to.be.equal(MESSAGE); 79 | expect(error.code).to.be.equal(400); 80 | expect(error.className).to.be.equal('bad-request'); 81 | } 82 | }); 83 | 84 | it('It should return migration type feathers error for P3000 to P4000', async () => { 85 | const code = 'P3000'; 86 | const ERROR = {code, ...ERROR_BODY}; 87 | try { 88 | const error = new Prisma.PrismaClientKnownRequestError(MESSAGE, ERROR); 89 | errorHandler(error, 'findUnique'); 90 | } catch (error) { 91 | assert.ok(error instanceof FeathersError); 92 | expect(error.name).to.be.equal('GeneralError'); 93 | expect(error.message).to.be.equal(MESSAGE); 94 | expect(error.code).to.be.equal(500); 95 | expect(error.className).to.be.equal('general-error'); 96 | } 97 | }); 98 | 99 | it('It should return introspection type feathers error for P4000 to P5000', async () => { 100 | const code = 'P4000'; 101 | const ERROR = {code, ...ERROR_BODY}; 102 | try { 103 | const error = new Prisma.PrismaClientKnownRequestError(MESSAGE, ERROR); 104 | errorHandler(error, 'findUnique'); 105 | } catch (error) { 106 | assert.ok(error instanceof FeathersError); 107 | expect(error.name).to.be.equal('GeneralError'); 108 | expect(error.message).to.be.equal(MESSAGE); 109 | expect(error.code).to.be.equal(500); 110 | expect(error.className).to.be.equal('general-error'); 111 | } 112 | }); 113 | 114 | it('It should return default bad request type feathers error for > P5000', async () => { 115 | const code = 'P5000'; 116 | const ERROR = {code, ...ERROR_BODY}; 117 | try { 118 | const error = new Prisma.PrismaClientKnownRequestError(MESSAGE, ERROR); 119 | errorHandler(error, 'findUnique'); 120 | } catch (error) { 121 | assert.ok(error instanceof FeathersError); 122 | expect(error.name).to.be.equal('BadRequest'); 123 | expect(error.message).to.be.equal(MESSAGE); 124 | expect(error.code).to.be.equal(400); 125 | expect(error.className).to.be.equal('bad-request'); 126 | } 127 | }); 128 | 129 | it('It should return prisma errors for PrismaClientValidationError and methods 4 methods', async () => { 130 | const code = 'P2025'; 131 | const ERROR = {code, ...ERROR_BODY}; 132 | const prismaMethods = ['findUnique', 'remove', 'update', 'delete']; 133 | for (const prismaMethod of prismaMethods) { 134 | try { 135 | const error = new Prisma.PrismaClientValidationError(MESSAGE, ERROR); 136 | errorHandler(error, prismaMethod); 137 | } catch (error) { 138 | assert.ok(error instanceof FeathersError); 139 | expect(error.name).to.be.equal('NotFound'); 140 | expect(error.message).to.be.equal('Record not found.'); 141 | expect(error.code).to.be.equal(404); 142 | expect(error.className).to.be.equal('not-found'); 143 | } 144 | } 145 | try { 146 | const error = new Prisma.PrismaClientValidationError(MESSAGE, ERROR); 147 | errorHandler(error, 'unknownMethod'); 148 | } catch (error) { 149 | assert.ok(error instanceof FeathersError); 150 | expect(error.name).to.be.equal('GeneralError'); 151 | expect(error.message).to.be.equal(MESSAGE); 152 | expect(error.code).to.be.equal(500); 153 | expect(error.className).to.be.equal('general-error'); 154 | } 155 | }); 156 | 157 | it('It should return general feathers error for all other errors.', async () => { 158 | try { 159 | const error = new Error(MESSAGE); 160 | errorHandler(error, 'findUnique'); 161 | } catch (error) { 162 | assert.ok(error instanceof FeathersError); 163 | expect(error.name).to.be.equal('GeneralError'); 164 | expect(error.message).to.be.equal(MESSAGE); 165 | expect(error.code).to.be.equal(500); 166 | expect(error.className).to.be.equal('general-error'); 167 | } 168 | }); 169 | }); 170 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | const { service, PrismaService, prismaService } = require('../dist'); 3 | const feathers = require('@feathersjs/feathers'); 4 | const adapterTests = require('@feathersjs/adapter-tests'); 5 | const errors = require('@feathersjs/errors'); 6 | const { PrismaClient } = require('@prisma/client'); 7 | 8 | const testSuite = adapterTests([ 9 | '.options', 10 | '.events', 11 | '._get', 12 | '._find', 13 | '._create', 14 | '._update', 15 | '._patch', 16 | '._remove', 17 | '.get', 18 | '.get + $select', 19 | '.get + id + query', 20 | '.get + NotFound', 21 | '.find', 22 | '.remove', 23 | '.remove + $select', 24 | '.remove + id + query', 25 | '.remove + multi', 26 | '.update', 27 | '.update + $select', 28 | '.update + id + query', 29 | '.update + NotFound', 30 | '.update + query + NotFound', 31 | '.patch', 32 | '.patch + $select', 33 | '.patch + id + query', 34 | '.patch multiple', 35 | '.patch + NotFound', 36 | '.patch multi query same', 37 | '.patch multi query changed', 38 | '.patch + query + NotFound', 39 | '.create', 40 | '.create + $select', 41 | '.create multi', 42 | 'internal .find', 43 | 'internal .get', 44 | 'internal .create', 45 | 'internal .update', 46 | 'internal .patch', 47 | 'internal .remove', 48 | '.find + equal', 49 | '.find + equal multiple', 50 | '.find + $sort', 51 | '.find + $sort + string', 52 | '.find + $limit', 53 | '.find + $limit 0', 54 | '.find + $skip', 55 | '.find + $select', 56 | '.find + $or', 57 | '.find + $in', 58 | '.find + $nin', 59 | '.find + $lt', 60 | '.find + $lte', 61 | '.find + $gt', 62 | '.find + $gte', 63 | '.find + $ne', 64 | '.find + $gt + $lt + $sort', 65 | '.find + $or nested + $sort', 66 | '.find + paginate', 67 | '.find + paginate + $limit + $skip', 68 | '.find + paginate + $limit 0', 69 | '.find + paginate + params', 70 | '.get + id + query id', 71 | '.remove + id + query id', 72 | '.update + id + query id', 73 | '.patch + id + query id' 74 | ]); 75 | 76 | const app = feathers(); 77 | const prismaClient = new PrismaClient(); 78 | 79 | try { 80 | prismaClient.$connect(); 81 | } catch (e) { 82 | console.error(e); 83 | } 84 | 85 | const users = prismaService({ 86 | model: 'user', 87 | events: ['testing'], 88 | whitelist: ['$eager', '$prisma'], 89 | }, prismaClient); 90 | 91 | const people = prismaService({ 92 | model: 'people', 93 | events: ['testing'], 94 | }, prismaClient); 95 | 96 | const peopleId = prismaService({ 97 | model: 'peopleId', 98 | id: 'customid', 99 | events: ['testing'], 100 | }, prismaClient); 101 | 102 | const todos = prismaService({ 103 | model: 'todo', 104 | multi: ['create', 'patch', 'remove'], 105 | whitelist: ['$eager'], 106 | }, prismaClient); 107 | 108 | app.use('/users', users); 109 | app.use('/people', people); 110 | app.use('/people-customid', peopleId); 111 | app.use('/todos', todos); 112 | 113 | 114 | describe('Feathers Prisma Service', () => { 115 | describe('Initialization', () => { 116 | it('clears database', async () => { 117 | await prismaClient.$connect(); 118 | await prismaClient.people.deleteMany({}); 119 | await prismaClient.user.deleteMany({}); 120 | await prismaClient.peopleId.deleteMany({}); 121 | await prismaClient.todo.deleteMany({}); 122 | }); 123 | 124 | describe('when missing a model', () => { 125 | it('throws an error', () => 126 | expect(service.bind(null, {}, prismaClient)) 127 | .to.throw(/You must provide a model string/) 128 | ); 129 | }); 130 | 131 | describe('when model is not included in prisma client', () => { 132 | it('throws an error', () => 133 | expect(service.bind(null, { model: 'test' }, prismaClient)) 134 | .to.throw('No model with name test found in prisma client.') 135 | ); 136 | }); 137 | 138 | describe('test basic functionality of exported members', () => { 139 | it('class and functions in parity', () => { 140 | const peopleService = app.service('people'); 141 | expect(typeof service).to.equal('function', 'It worked'); 142 | expect(typeof prismaService).to.equal('function', 'It worked'); 143 | expect(peopleService.Model) 144 | .to.equal(new PrismaService({ model: 'people' }, prismaClient).Model); 145 | expect(peopleService.Model) 146 | .to.equal(prismaService({ model: 'people' }, prismaClient).Model); 147 | expect(prismaService({ model: 'people', id: 'customid' }, prismaClient).options.id) 148 | .to.not.equal(peopleService.options.id); 149 | }); 150 | }); 151 | }); 152 | 153 | describe('Custom tests', () => { 154 | const usersService = app.service('users'); 155 | const todosService = app.service('todos'); 156 | const peopleService = app.service('people'); 157 | 158 | let data; 159 | beforeEach(async () => { 160 | data = await usersService.create({ 161 | name: 'Max Power', 162 | age: 19, 163 | todos: { 164 | create: [ 165 | { title: 'Todo1', prio: 1 } 166 | ], 167 | } 168 | }, { 169 | query: { 170 | $eager: ['todos'], 171 | }, 172 | }); 173 | }); 174 | 175 | afterEach(async () => { 176 | await todosService.remove(null, { userId: data.id }); 177 | await usersService.remove(data.id); 178 | }); 179 | 180 | describe('relations', () => { 181 | it('creates with related items', () => { 182 | expect(data.todos.length).to.equal(1); 183 | }); 184 | it('.find + eager loading related item', async () => { 185 | const result = await todosService.find({ 186 | query: { 187 | $eager: { 188 | user: ['name'], 189 | }, 190 | }, 191 | }); 192 | expect(result[0].user.id).to.equal(result[0].userId); 193 | }); 194 | it('.find + deep eager loading related item', async () => { 195 | const result = await todosService.find({ 196 | query: { 197 | $eager: [['user', ['todos', ['user']]]], 198 | }, 199 | }); 200 | expect(result[0].user.todos[0].user.id).to.equal(result[0].user.todos[0].userId); 201 | }); 202 | 203 | it('.find + throws $eager type error', async () => { 204 | try { 205 | await todosService.find({ 206 | query: { 207 | $eager: [true], 208 | }, 209 | }); 210 | console.error('never goes here'); 211 | } catch (e) { 212 | expect(e.code).to.be.equal('FP1001'); 213 | } 214 | }); 215 | }); 216 | 217 | describe('custom query', () => { 218 | beforeEach(async () => { 219 | await todosService.create([ 220 | { title: 'Lorem', prio: 1, userId: data.id }, 221 | { title: 'Lorem Ipsum', prio: 1, userId: data.id, tag1: 'TEST' }, 222 | { title: '[TODO]', prio: 1, userId: data.id, tag1: 'TEST2' }, 223 | ]); 224 | }); 225 | 226 | it('.find + $contains', async () => { 227 | const results = await todosService.find({ 228 | query: { 229 | title: { 230 | $contains: 'lorem', 231 | }, 232 | }, 233 | }); 234 | expect(results.length).to.equal(2); 235 | }); 236 | 237 | it('.find + $startsWith', async () => { 238 | const results = await todosService.find({ 239 | query: { 240 | title: { 241 | $startsWith: 'lorem', 242 | }, 243 | }, 244 | }); 245 | expect(results.length).to.equal(2); 246 | }); 247 | 248 | it('.find + $endsWith', async () => { 249 | const results = await todosService.find({ 250 | query: { 251 | title: { 252 | $endsWith: 'o]', 253 | }, 254 | }, 255 | }); 256 | expect(results.length).to.equal(1); 257 | }); 258 | 259 | it('.find + query field "null" value', async () => { 260 | const results = await todosService.find({ 261 | query: { 262 | tag1: 'null', 263 | }, 264 | }); 265 | expect(results.length).to.equal(2); 266 | }); 267 | 268 | it('.find + $prisma + query related items', async () => { 269 | await todosService.create([ 270 | { title: 'Todo2', prio: 2, userId: data.id }, 271 | { title: 'Todo3', prio: 4, done: true, userId: data.id }, 272 | ]); 273 | const result = await usersService.find({ 274 | query: { 275 | todos: { 276 | $prisma: { 277 | some: { 278 | prio: 2, 279 | }, 280 | }, 281 | }, 282 | $eager: { 283 | todos: true, 284 | }, 285 | }, 286 | }); 287 | const result2 = await usersService.find({ 288 | query: { 289 | todos: { 290 | $prisma: { 291 | every: { 292 | done: true, 293 | }, 294 | }, 295 | }, 296 | }, 297 | }); 298 | expect(result).to.have.lengthOf(1); 299 | expect(result2).to.have.lengthOf(0); 300 | }); 301 | 302 | it('.find + $and', async () => { 303 | await todosService.create([ 304 | { title: 'Todo2', prio: 2, userId: data.id }, 305 | { title: 'Todo3', prio: 4, done: true, userId: data.id }, 306 | ]); 307 | 308 | const result = await todosService.find({ 309 | query: { 310 | $and: [{tag1: {$in: ['TEST', 'TEST2']}}] 311 | }, 312 | }); 313 | 314 | expect(result).to.have.lengthOf(2); 315 | }); 316 | 317 | it('.find + $and + merge with equals', async () => { 318 | await todosService.create([ 319 | { title: 'Todo2', prio: 2, userId: data.id }, 320 | { title: 'Todo3', prio: 4, done: true, userId: data.id }, 321 | ]); 322 | 323 | const result = await todosService.find({ 324 | query: { 325 | tag1: 'TEST', 326 | $and: [{tag1: {$in: ['TEST', 'TEST2']}}], 327 | }, 328 | }); 329 | 330 | expect(result).to.have.lengthOf(1); 331 | }); 332 | 333 | it('.find + $and + merge with normal query', async () => { 334 | await todosService.create([ 335 | { title: 'Todo2', prio: 2, userId: data.id }, 336 | { title: 'Todo3', prio: 4, done: true, userId: data.id }, 337 | ]); 338 | 339 | const result = await todosService.find({ 340 | query: { 341 | tag1: { $ne: 'TEST' }, 342 | $and: [{tag1: {$in: ['TEST', 'TEST2']}}], 343 | }, 344 | }); 345 | 346 | expect(result).to.have.lengthOf(1); 347 | }); 348 | 349 | it('.find + $and', async () => { 350 | await todosService.create([ 351 | { title: 'Todo2', prio: 2, userId: data.id }, 352 | { title: 'Todo3', prio: 4, done: true, userId: data.id }, 353 | ]); 354 | 355 | const result = await todosService.find({ 356 | query: { 357 | $and: [{tag1: 'TEST'}], 358 | }, 359 | }); 360 | 361 | expect(result).to.have.lengthOf(1); 362 | }); 363 | 364 | it('.get + multiple id queries + NotFound', async () => { 365 | try { 366 | await todosService.create([ 367 | { title: 'Todo2', prio: 2, userId: data.id }, 368 | { title: 'Todo3', prio: 4, done: true, userId: data.id }, 369 | ]); 370 | const results = await todosService.find(); 371 | const inIds = [results[1].id, results[2].id]; 372 | 373 | await todosService.get(results[0].id, { 374 | query: { 375 | id: { 376 | $in: inIds, 377 | } 378 | }, 379 | }); 380 | } catch (e) { 381 | expect(e.code).to.be.equal(404); 382 | } 383 | }); 384 | 385 | it('.get + multiple id queries + result', async () => { 386 | await todosService.create([ 387 | { title: 'Todo2', prio: 2, userId: data.id }, 388 | { title: 'Todo3', prio: 4, done: true, userId: data.id }, 389 | ]); 390 | const results = await todosService.find(); 391 | const inIds = [results[1].id, results[0].id]; 392 | 393 | const result = await todosService.get(results[0].id, { 394 | query: { 395 | id: { 396 | $in: inIds, 397 | } 398 | }, 399 | }); 400 | expect(result.id).to.be.equal(results[0].id); 401 | }); 402 | 403 | it('.get + additional queries + result', async () => { 404 | const created = await todosService.create([ 405 | { title: 'Todo2', prio: 2, userId: data.id, tag1: 'TEST3' }, 406 | { title: 'Todo3', prio: 4, done: true, userId: data.id, tag1: 'TEST' }, 407 | ]); 408 | 409 | const result = await todosService.get(created[0].id, { 410 | query: { 411 | $and: [{tag1: {$nin: ['TEST', 'TEST2']}}], 412 | }, 413 | }); 414 | expect(result.id).to.be.equal(created[0].id); 415 | }); 416 | 417 | it('.get + id equals query with same id + result', async () => { 418 | await todosService.create([ 419 | { title: 'Todo2', prio: 2, userId: data.id }, 420 | { title: 'Todo3', prio: 4, done: true, userId: data.id }, 421 | ]); 422 | const results = await todosService.find(); 423 | 424 | const result = await todosService.get(results[0].id, { 425 | query: { 426 | $and: [{id: results[0].id}], 427 | }, 428 | }); 429 | expect(result.id).to.be.equal(results[0].id); 430 | }); 431 | 432 | it('.get + id equals query with other id + NotFound', async () => { 433 | try { 434 | await todosService.create([ 435 | { title: 'Todo2', prio: 2, userId: data.id }, 436 | { title: 'Todo3', prio: 4, done: true, userId: data.id }, 437 | ]); 438 | const results = await todosService.find(); 439 | 440 | await todosService.get(results[0].id, { 441 | query: { 442 | $and: [{id: results[1].id}], 443 | }, 444 | }); 445 | } catch (e) { 446 | expect(e.code).to.be.equal(404); 447 | } 448 | }); 449 | 450 | it('.remove + multiple id queries + NotFound', async () => { 451 | try { 452 | await todosService.create([ 453 | { title: 'Todo2', prio: 2, userId: data.id }, 454 | { title: 'Todo3', prio: 4, done: true, userId: data.id }, 455 | ]); 456 | const results = await todosService.find(); 457 | const inIds = [results[1].id, results[2].id]; 458 | 459 | await todosService.remove(results[0].id, { 460 | query: { 461 | $and: [{id: {$in: inIds}}], 462 | }, 463 | }); 464 | } catch (e) { 465 | expect(e.code).to.be.equal(404); 466 | } 467 | }); 468 | 469 | it('.remove + multiple id queries + result', async () => { 470 | await todosService.create([ 471 | { title: 'Todo2', prio: 2, userId: data.id }, 472 | { title: 'Todo3', prio: 4, done: true, userId: data.id }, 473 | ]); 474 | const results = await todosService.find(); 475 | const inIds = [results[1].id, results[0].id]; 476 | 477 | const result = await todosService.remove(results[0].id, { 478 | query: { 479 | $and: [{id: {$in: inIds}}], 480 | }, 481 | }); 482 | expect(result.id).to.be.equal(results[0].id); 483 | }); 484 | 485 | it('.update + multiple id queries + NotFound', async () => { 486 | try { 487 | await todosService.create([ 488 | { title: 'Todo2', prio: 2, userId: data.id }, 489 | { title: 'Todo3', prio: 4, done: true, userId: data.id }, 490 | ]); 491 | const results = await todosService.find(); 492 | const inIds = [results[1].id, results[2].id]; 493 | 494 | await todosService.update(results[0].id, { 495 | tag1: 'NEW TAG', 496 | }, { 497 | query: { 498 | $and: [{id: {$in: inIds}}], 499 | }, 500 | }); 501 | } catch (e) { 502 | expect(e.code).to.be.equal(404); 503 | } 504 | }); 505 | 506 | it('.update + multiple id queries + result', async () => { 507 | await todosService.create([ 508 | { title: 'Todo2', prio: 2, userId: data.id }, 509 | { title: 'Todo3', prio: 4, done: true, userId: data.id }, 510 | ]); 511 | const results = await todosService.find(); 512 | const inIds = [results[1].id, results[0].id]; 513 | 514 | const result = await todosService.update(results[0].id, { 515 | tag1: 'NEW TAG', 516 | }, { 517 | query: { 518 | $and: [{id: {$in: inIds}}], 519 | }, 520 | }); 521 | expect(result.tag1).to.be.equal('NEW TAG'); 522 | }); 523 | 524 | it('.patch + multiple id queries + result', async () => { 525 | await todosService.create([ 526 | { title: 'Todo2', prio: 2, userId: data.id }, 527 | { title: 'Todo3', prio: 4, done: true, userId: data.id }, 528 | ]); 529 | const results = await todosService.find(); 530 | const inIds = [results[1].id, results[0].id]; 531 | 532 | const result = await todosService.patch(results[0].id, { 533 | tag1: 'NEW TAG', 534 | }, { 535 | query: { 536 | $and: [{id: {$in: inIds}}], 537 | }, 538 | }); 539 | expect(result.tag1).to.be.equal('NEW TAG'); 540 | }); 541 | 542 | it('.patch + multiple id queries + NotFound', async () => { 543 | try { 544 | await todosService.create([ 545 | { title: 'Todo2', prio: 2, userId: data.id }, 546 | { title: 'Todo3', prio: 4, done: true, userId: data.id }, 547 | ]); 548 | const results = await todosService.find(); 549 | const inIds = [results[1].id, results[2].id]; 550 | 551 | await todosService.update(results[0].id, { 552 | tag1: 'NEW TAG', 553 | }, { 554 | query: { 555 | $and: [{id: {$in: inIds}}], 556 | }, 557 | }); 558 | } catch (e) { 559 | expect(e.code).to.be.equal(404); 560 | } 561 | }); 562 | }); 563 | }); 564 | 565 | testSuite(app, errors, 'users', 'id'); 566 | testSuite(app, errors, 'people', 'id'); 567 | testSuite(app, errors, 'people-customid', 'customid'); 568 | }); 569 | 570 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es6", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | 26 | /* Modules */ 27 | "module": "CommonJS", /* Specify what module code is generated. */ 28 | // "rootDir": "./", /* Specify the root folder within your source files. */ 29 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 32 | // "rootDirs": [], /* Allow multiple folders to be treated xas one when resolving modules. */ 33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 34 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 35 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 36 | // "resolveJsonModule": true, /* Enable importing .json files */ 37 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 38 | 39 | /* JavaScript Support */ 40 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 41 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 43 | 44 | /* Emit */ 45 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 46 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 47 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 48 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 49 | // "outFile": "./lib/index.js", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 50 | "outDir": "./dist", /* Specify an output folder for all emitted files. */ 51 | // "removeComments": true, /* Disable emitting comments. */ 52 | // "noEmit": true, /* Disable emitting files from a compilation. */ 53 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 54 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 55 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 56 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 60 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 61 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 62 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 63 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 64 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 65 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 66 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 67 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 68 | 69 | /* Interop Constraints */ 70 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 71 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 72 | "esModuleInterop": false, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ 73 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 74 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 75 | 76 | /* Type Checking */ 77 | "strict": true, /* Enable all strict type-checking options. */ 78 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 79 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 80 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 81 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 82 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 83 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 84 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 85 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 86 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 87 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 88 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 89 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 90 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 91 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 92 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 93 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 94 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 95 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 96 | 97 | /* Completeness */ 98 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 99 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 100 | }, 101 | "exclude": [ 102 | "node_modules", 103 | "example" 104 | ], 105 | "include": ["src/*", "test/newTests.test.js"], 106 | } 107 | --------------------------------------------------------------------------------