├── .czrc ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── README.md ├── Taskfile ├── eslint.config.mjs ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── prettier.config.mjs ├── release.config.cjs ├── schema.prisma ├── src ├── index.spec.ts └── index.ts ├── stryker.conf.mjs └── tsconfig.json /.czrc: -------------------------------------------------------------------------------- 1 | { 2 | "path": "./node_modules/cz-conventional-changelog", 3 | "disableScopeLowerCase": false, 4 | "disableSubjectLowerCase": true 5 | } 6 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | on: 3 | - push 4 | - pull_request 5 | 6 | jobs: 7 | build: 8 | name: 'Test on Node ${{ matrix.node }} and ${{ matrix.os }}' 9 | runs-on: '${{ matrix.os }}' 10 | strategy: 11 | # max-parallel: 1 12 | matrix: 13 | node: 14 | - 22 15 | os: 16 | - ubuntu-latest 17 | # - windows-latest 18 | # - macOS-latest 19 | steps: 20 | - name: 'Checkout Repository' 21 | uses: actions/checkout@v3 22 | 23 | - name: 'Setup Node ${{ matrix.node }}' 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: '${{ matrix.node }}' 27 | 28 | - name: 'Install Depependencies' 29 | run: npm install --force 30 | 31 | - name: 'Tests' 32 | run: | 33 | npm run test 34 | 35 | - name: 'Mutation Tests' 36 | run: | 37 | npm run test:m 38 | 39 | release: 40 | name: 'Release' 41 | runs-on: ubuntu-latest 42 | needs: build 43 | if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/next') 44 | 45 | steps: 46 | - name: 'Checkout Repository' 47 | uses: actions/checkout@v3 48 | 49 | - name: 'Setup Node' 50 | uses: actions/setup-node@v3 51 | with: 52 | node-version: 22 53 | 54 | - name: 'Install Depependencies' 55 | run: | 56 | npm install --force 57 | 58 | - name: 'Build' 59 | run: | 60 | npm run build 61 | 62 | - name: 'Release' 63 | run: | 64 | npx semantic-release 65 | env: 66 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 67 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### https://raw.github.com/github/gitignore/5d896f6791c4257b74696714c66b2530b8d95a51/Node.gitignore 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | # nyc test coverage 18 | .nyc_output 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | # Bower dependency directory (https://bower.io/) 22 | bower_components 23 | # node-waf configuration 24 | .lock-wscript 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | # Dependency directories 28 | node_modules/ 29 | jspm_packages/ 30 | # Typescript v1 declaration files 31 | typings/ 32 | # Optional npm cache directory 33 | .npm 34 | # Optional eslint cache 35 | .eslintcache 36 | # Optional REPL history 37 | .node_repl_history 38 | # Output of 'npm pack' 39 | *.tgz 40 | # Yarn Integrity file 41 | .yarn-integrity 42 | # dotenv environment variables file 43 | .env 44 | # Custom 45 | dist/ 46 | ~* 47 | .idea 48 | .awcache 49 | .vscode 50 | .rts2_cache_* 51 | .stryker-tmp 52 | reports 53 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 3.0.1 (2025-04-05) 2 | 3 | * build: Update scripts ([06c067b](https://github.com/unlight/prisma-graphql-type-decimal/commit/06c067b)) 4 | * fix: Update peer dependencies ([4248fe0](https://github.com/unlight/prisma-graphql-type-decimal/commit/4248fe0)) 5 | * chore: Updated packages ([774917b](https://github.com/unlight/prisma-graphql-type-decimal/commit/774917b)) 6 | 7 | ## [3.0.0](https://github.com/unlight/prisma-graphql-type-decimal/compare/v2.0.6...v3.0.0) (2023-04-02) 8 | 9 | 10 | ### ⚠ BREAKING CHANGES 11 | 12 | * `Decimal` imports from `@prisma/client/runtime/library`, require Prisma v4.12+ 13 | 14 | ### Bug Fixes 15 | 16 | * Deprecation warning `imports from "@prisma/client/runtime" are deprecated` ([76ffa5d](https://github.com/unlight/prisma-graphql-type-decimal/commit/76ffa5d3e663b6b9befcbaf1a6e2d0e7ee2875d8)) 17 | 18 | ## [2.0.6](https://github.com/unlight/prisma-graphql-type-decimal/compare/v2.0.5...v2.0.6) (2023-01-19) 19 | 20 | 21 | ### Bug Fixes 22 | 23 | * Correct semver range ([7b7bf2e](https://github.com/unlight/prisma-graphql-type-decimal/commit/7b7bf2ef15ff3274299209851031d7c756d13a57)) 24 | 25 | ## [2.0.5](https://github.com/unlight/prisma-graphql-type-decimal/compare/v2.0.4...v2.0.5) (2023-01-12) 26 | 27 | 28 | ### Bug Fixes 29 | 30 | * Decimal can be null ([04a27f3](https://github.com/unlight/prisma-graphql-type-decimal/commit/04a27f31582bb70a407ded5bb76897bebb53b6f0)), closes [#7](https://github.com/unlight/prisma-graphql-type-decimal/issues/7) 31 | 32 | ## [2.0.4](https://github.com/unlight/prisma-graphql-type-decimal/compare/v2.0.3...v2.0.4) (2022-07-09) 33 | 34 | 35 | ### Bug Fixes 36 | 37 | * **esm:** Targets must start with `./` ([8ff34b6](https://github.com/unlight/prisma-graphql-type-decimal/commit/8ff34b6d07b1cdf245cc0548ef4c86ac897045b1)), closes [#4](https://github.com/unlight/prisma-graphql-type-decimal/issues/4) 38 | 39 | ## [2.0.3](https://github.com/unlight/prisma-graphql-type-decimal/compare/v2.0.2...v2.0.3) (2022-07-08) 40 | 41 | 42 | ### Bug Fixes 43 | 44 | * **esm:** Fix import path ([8cdae9e](https://github.com/unlight/prisma-graphql-type-decimal/commit/8cdae9ed1f7b1307d4777933a74068b4a17e0d7a)), closes [#3](https://github.com/unlight/prisma-graphql-type-decimal/issues/3) 45 | 46 | ### [2.0.2](https://github.com/unlight/prisma-graphql-type-decimal/compare/v2.0.1...v2.0.2) (2022-05-22) 47 | 48 | 49 | ### Bug Fixes 50 | 51 | * Transform to decimal ([ab2805d](https://github.com/unlight/prisma-graphql-type-decimal/commit/ab2805d989e9a346af80a9f7184a71ec00c32241)) 52 | 53 | ### [2.0.1](https://github.com/unlight/prisma-graphql-type-decimal/compare/v2.0.0...v2.0.1) (2022-05-21) 54 | 55 | 56 | ### Bug Fixes 57 | 58 | * **class-transform:** Decimal value object factory ([edf2723](https://github.com/unlight/prisma-graphql-type-decimal/commit/edf27235769e7368a5caac314cfcecb168af70fc)) 59 | 60 | ## [2.0.0](https://github.com/unlight/prisma-graphql-type-decimal/compare/v1.0.0...v2.0.0) (2022-02-20) 61 | 62 | 63 | ### ⚠ BREAKING CHANGES 64 | 65 | * GraphQL >= 16, Prisma >= 3.8 66 | 67 | ### Miscellaneous Chores 68 | 69 | * Updated dependencies ([7e2cdf5](https://github.com/unlight/prisma-graphql-type-decimal/commit/7e2cdf5a57c284eb1fcbadf26b19d172c2901221)) 70 | 71 | ## 1.0.0 (2021-04-23) 72 | 73 | 74 | ### Features 75 | 76 | * First implementation ([2164365](https://github.com/unlight/prisma-graphql-type-decimal/commit/2164365465bb6b780c14a9dfbddad4f9eeb49e70)) 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # prisma-graphql-type-decimal 2 | 3 | GraphQL type for Prisma's Decimal scalar, wrapper around [decimal.js](https://github.com/MikeMcl/decimal.js/) 4 | Created because `@prisma/client` has bundled `decimal.js` 5 | 6 | ## Install 7 | 8 | ```sh 9 | npm install prisma-graphql-type-decimal 10 | 11 | ``` 12 | 13 | ## Usage 14 | 15 | Example usage with NestJS GraphQL code first approach: 16 | 17 | ```ts 18 | import { Decimal } from '@prisma/client/runtime/library.js'; 19 | import { transformToDecimal } from 'prisma-graphql-type-decimal'; 20 | import { Type, Transform } from 'class-transformer'; 21 | 22 | @ObjectType() 23 | export class User { 24 | /** 25 | * Trick to avoid error when using `@Field(() => GraphQLDecimal)` 26 | */ 27 | @Field(() => GraphQLDecimal) 28 | @Type(() => Object) 29 | @Transform(transformToDecimal) 30 | money: Decimal; 31 | 32 | @Type(() => Object) 33 | @Transform(transformToDecimal) 34 | moneys!: Array; 35 | } 36 | 37 | // In nested object 38 | class Transfers { 39 | @Type(() => Object) 40 | @Transform(transformToDecimal) 41 | moneys!: Array; 42 | } 43 | class Container { 44 | @Type(() => Transfers) 45 | set!: Transfers; 46 | } 47 | ``` 48 | 49 | ## License 50 | 51 | [MIT License](https://opensource.org/licenses/MIT) (c) 2023 52 | -------------------------------------------------------------------------------- /Taskfile: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | PATH="$PWD/node_modules/.bin":$PATH 3 | set -e 4 | 5 | buildMicrobundle4() { 6 | set -x 7 | rm -rfv dist 8 | microbundle -i src/index.ts --tsconfig tsconfig.json --no-sourcemap --no-compress --target node -f cjs,esm -o dist 9 | name=$(cat package.json | jq -r '.name') 10 | cp -v README.md package.json dist 11 | cd dist 12 | mv "$name.js" "$name.cjs" 13 | mv index.d.ts "$name.d.ts" 14 | cat ../package.json | jq --arg name "$name" ' 15 | .main = "./\($name).cjs" | 16 | .module = "./\($name).mjs" | 17 | .types = "./\($name).d.ts" | 18 | .exports.".".import = "./\($name).mjs" | 19 | .exports.".".require = "./\($name).cjs" 20 | ' > package.json 21 | cd .. 22 | set +x 23 | } 24 | 25 | "$@" 26 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import 'eslint-plugin-only-warn'; 2 | 3 | import globals from 'globals'; 4 | import pluginJs from '@eslint/js'; 5 | import tseslint from 'typescript-eslint'; 6 | import prettier from 'eslint-plugin-prettier/recommended'; 7 | import unicorn from 'eslint-plugin-unicorn'; 8 | import perfectionist from 'eslint-plugin-perfectionist'; 9 | import simpleImportSort from 'eslint-plugin-simple-import-sort'; 10 | import wixEditor from 'eslint-plugin-wix-editor'; 11 | import { fixupPluginRules } from '@eslint/compat'; 12 | 13 | /** @type {import('@typescript-eslint/utils').TSESLint.FlatConfig.ConfigFile} */ 14 | export default [ 15 | pluginJs.configs.recommended, 16 | ...tseslint.configs.recommended, 17 | ...tseslint.configs.recommendedTypeChecked, 18 | prettier, 19 | { 20 | ignores: [ 21 | 'dist/', 22 | 'coverage/', 23 | '@generated/**', 24 | '*.config.[cm]js', 25 | '.*rc.js', 26 | ], 27 | languageOptions: { 28 | globals: globals.node, 29 | parserOptions: { 30 | project: ['./tsconfig.json'], 31 | warnOnUnsupportedTypeScriptVersion: false, 32 | tsconfigRootDir: import.meta.dirname, 33 | }, 34 | }, 35 | rules: { 36 | 'max-lines': [1, { max: 300 }], 37 | 'max-params': [1, { max: 5 }], 38 | 'no-unneeded-ternary': [1], 39 | }, 40 | }, 41 | { 42 | plugins: { 43 | 'wix-editor': fixupPluginRules(wixEditor), 44 | }, 45 | rules: { 46 | 'wix-editor/no-instanceof-array': 1, 47 | 'wix-editor/no-not-not': 1, 48 | 'wix-editor/no-unneeded-match': 1, 49 | 'wix-editor/prefer-filter': 1, 50 | 'wix-editor/prefer-ternary': 1, 51 | 'wix-editor/return-boolean': 1, 52 | 'wix-editor/simplify-boolean-expression': 1, 53 | }, 54 | }, 55 | { 56 | ...unicorn.configs.recommended, 57 | rules: { 58 | 'unicorn/prevent-abbreviations': [ 59 | 'warn', 60 | { 61 | replacements: { 62 | args: false, 63 | }, 64 | }, 65 | ], 66 | }, 67 | }, 68 | { 69 | plugins: { 70 | perfectionist, 71 | }, 72 | rules: { 73 | 'perfectionist/sort-objects': [ 74 | 'warn', 75 | { 76 | type: 'natural', 77 | order: 'asc', 78 | }, 79 | ], 80 | }, 81 | }, 82 | { 83 | plugins: { 84 | 'simple-import-sort': simpleImportSort, 85 | }, 86 | rules: { 87 | 'simple-import-sort/imports': 'warn', 88 | 'simple-import-sort/exports': 'warn', 89 | }, 90 | }, 91 | { 92 | files: ['**/*.spec.ts', '**/*.e2e-spec.ts'], 93 | rules: { 94 | 'consistent-return': 0, 95 | 'max-lines': 0, 96 | '@typescript-eslint/no-explicit-any': 0, 97 | '@typescript-eslint/no-floating-promises': 0, 98 | '@typescript-eslint/no-non-null-assertion': 0, 99 | '@typescript-eslint/camelcase': 0, 100 | 'import/max-dependencies': 0, 101 | }, 102 | }, 103 | ]; 104 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prisma-graphql-type-decimal", 3 | "version": "0.0.0-dev", 4 | "license": "MIT", 5 | "description": "GraphQL type for Prisma's Decimal scalar, wrapper around decimal.js", 6 | "main": "./dist/index.cjs", 7 | "module": "./dist/index.mjs", 8 | "types": "./dist/index.d.ts", 9 | "exports": { 10 | ".": { 11 | "import": "./dist/index.mjs", 12 | "require": "./dist/index.cjs" 13 | } 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/unlight/prisma-graphql-type-decimal.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/unlight/prisma-graphql-type-decimal/issues" 21 | }, 22 | "homepage": "https://github.com/unlight/prisma-graphql-type-decimal#readme", 23 | "scripts": { 24 | "test": "npm run eslint && npm run tscheck && npm run test:cov", 25 | "test:m": "node node_modules/@stryker-mutator/core/bin/stryker.js run", 26 | "test:r": "node -r ts-node/register node_modules/mocha/bin/mocha src/**/*.spec.ts", 27 | "test:cov": "c8 --reporter text --exclude \"**/*.spec.ts\" --exclude \"**/testing/**\" npm run test:r", 28 | "test:w": "npm run test:r -- --watch --watch-files src", 29 | "lint": "npm run eslint && npm run tscheck", 30 | "eslint": "node node_modules/eslint/bin/eslint \"src/**/*.{ts,tsx}\"", 31 | "eslint:w": "watchexec -w src \"npm run eslint\"", 32 | "eslint:fix": "npm run eslint -- --fix", 33 | "tscheck": "tsc --noEmit", 34 | "tscheck:w": "npm run tscheck -- --watch", 35 | "format:src": "prettier src --write", 36 | "commit_lint": "sh Taskfile commit_lint", 37 | "commit": "cz", 38 | "build": "sh Taskfile buildMicrobundle4", 39 | "test:d": "ndb -r ts-node/register/transpile-only node_modules/mocha/bin/mocha --no-timeouts --watch-files src/**/*.ts --watch src/**/*.spec.ts" 40 | }, 41 | "peerDependencies": { 42 | "@prisma/client": "4.12 - 6", 43 | "graphql": "16" 44 | }, 45 | "devDependencies": { 46 | "@commitlint/cli": "^19.8.0", 47 | "@commitlint/config-conventional": "^19.8.0", 48 | "@eslint/compat": "^1.2.8", 49 | "@eslint/js": "^9.24.0", 50 | "@prisma/client": "^5.0.0", 51 | "@semantic-release/changelog": "^6.0.3", 52 | "@semantic-release/git": "^10.0.1", 53 | "@stryker-mutator/api": "^8.7.1", 54 | "@stryker-mutator/core": "^8.7.1", 55 | "@stryker-mutator/mocha-runner": "^8.7.1", 56 | "@swc/core": "^1.11.16", 57 | "@swc/helpers": "^0.5.15", 58 | "@types/mocha": "^10.0.10", 59 | "@types/node": "^22.14.0", 60 | "@typescript-eslint/eslint-plugin": "^8.29.0", 61 | "@typescript-eslint/parser": "^8.29.0", 62 | "c8": "^10.1.3", 63 | "class-transformer": "^0.5.1", 64 | "commitizen": "^4.3.1", 65 | "cz-conventional-changelog": "^3.3.0", 66 | "cz-customizable": "^7.4.0", 67 | "eslint": "^9.24.0", 68 | "eslint-config-prettier": "^10.1.1", 69 | "eslint-plugin-etc": "^2.0.3", 70 | "eslint-plugin-only-warn": "^1.1.0", 71 | "eslint-plugin-perfectionist": "^4.11.0", 72 | "eslint-plugin-prettier": "^5.2.6", 73 | "eslint-plugin-simple-import-sort": "^12.1.1", 74 | "eslint-plugin-unicorn": "^58.0.0", 75 | "eslint-plugin-wix-editor": "^3.3.0", 76 | "expect": "^29.7.0", 77 | "globals": "^16.0.0", 78 | "graphql": "^16.6.0", 79 | "microbundle": "^0.15.1", 80 | "mocha": "^11.1.0", 81 | "precise-commits": "^1.0.2", 82 | "prettier": "^3.5.3", 83 | "prisma": "^6.5.0", 84 | "reflect-metadata": "^0.2.2", 85 | "semantic-release": "^24.2.3", 86 | "simplytyped": "^3.3.0", 87 | "stryker-cli": "^1.0.2", 88 | "ts-node": "^10.9.2", 89 | "tslib": "^2.8.1", 90 | "typescript": "^5.8.3", 91 | "typescript-eslint": "^8.29.0" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /prettier.config.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://prettier.io/docs/configuration 3 | * @type {import('prettier').Config} 4 | */ 5 | export default { 6 | printWidth: 80, 7 | trailingComma: 'all', 8 | tabWidth: 2, 9 | semi: true, 10 | singleQuote: true, 11 | arrowParens: 'avoid', 12 | }; 13 | -------------------------------------------------------------------------------- /release.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | [ 4 | '@semantic-release/commit-analyzer', 5 | { 6 | preset: 'conventionalcommits', 7 | }, 8 | ], 9 | [ 10 | '@semantic-release/release-notes-generator', 11 | { 12 | preset: 'conventionalcommits', 13 | }, 14 | ], 15 | '@semantic-release/changelog', 16 | [ 17 | '@semantic-release/npm', 18 | { 19 | pkgRoot: 'dist', 20 | }, 21 | ], 22 | '@semantic-release/github', 23 | '@semantic-release/git', 24 | ], 25 | }; 26 | -------------------------------------------------------------------------------- /schema.prisma: -------------------------------------------------------------------------------- 1 | datasource database { 2 | provider = "postgres" 3 | url = env("DATABASE_URL") 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | } 9 | 10 | model Dummy { 11 | id String @id 12 | created DateTime @default(now()) 13 | floaty Float 14 | int Int? 15 | float Float? 16 | bytes Bytes? 17 | decimal Decimal? 18 | bigInt BigInt? 19 | json Json? 20 | } 21 | -------------------------------------------------------------------------------- /src/index.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { Decimal } from '@prisma/client/runtime/library'; 4 | import { plainToClass, Transform, Type } from 'class-transformer'; 5 | import expect from 'expect'; 6 | import { graphql, GraphQLObjectType, GraphQLSchema } from 'graphql'; 7 | 8 | import { createDecimalFromObject, GraphQLDecimal, transformToDecimal } from '.'; 9 | 10 | it('smoke', async () => { 11 | const schema = new GraphQLSchema({ 12 | query: new GraphQLObjectType({ 13 | name: 'Query', 14 | fields: { 15 | decimal: { 16 | type: GraphQLDecimal, 17 | resolve: () => 1, 18 | }, 19 | }, 20 | }), 21 | }); 22 | 23 | expect( 24 | await graphql({ 25 | schema, 26 | source: /* GraphQL */ ` 27 | query { 28 | decimal 29 | } 30 | `, 31 | }), 32 | ).toBeTruthy(); 33 | }); 34 | 35 | it('echo', async () => { 36 | const schema = new GraphQLSchema({ 37 | query: new GraphQLObjectType({ 38 | name: 'Query', 39 | fields: { 40 | echo: { 41 | type: GraphQLDecimal, 42 | args: { 43 | num: { type: GraphQLDecimal }, 44 | }, 45 | resolve: (_root, args) => args.num, 46 | }, 47 | }, 48 | }), 49 | }); 50 | expect( 51 | await graphql({ 52 | schema, 53 | source: /* GraphQL */ ` 54 | query { 55 | float: echo(num: 0.1) 56 | int: echo(num: 2) 57 | string: echo(num: "3") 58 | } 59 | `, 60 | }), 61 | ).toEqual({ 62 | data: { 63 | float: '0.1', 64 | int: '2', 65 | string: '3', 66 | }, 67 | }); 68 | }); 69 | 70 | it('inc', async () => { 71 | const schema = new GraphQLSchema({ 72 | query: new GraphQLObjectType({ 73 | name: 'Query', 74 | fields: { 75 | inc: { 76 | type: GraphQLDecimal, 77 | args: { 78 | num: { type: GraphQLDecimal }, 79 | }, 80 | resolve: (_root, args) => new Decimal(0.1).add(args.num), 81 | }, 82 | }, 83 | }), 84 | }); 85 | expect( 86 | await graphql({ 87 | schema, 88 | source: /* GraphQL */ ` 89 | query { 90 | inc(num: 0.2) 91 | } 92 | `, 93 | }), 94 | ).toEqual({ 95 | data: { 96 | inc: '0.3', 97 | }, 98 | }); 99 | }); 100 | 101 | it('parse value', async () => { 102 | const schema = new GraphQLSchema({ 103 | query: new GraphQLObjectType({ 104 | name: 'Query', 105 | fields: { 106 | sum: { 107 | type: GraphQLDecimal, 108 | args: { 109 | a: { 110 | type: GraphQLDecimal, 111 | }, 112 | b: { 113 | type: GraphQLDecimal, 114 | }, 115 | }, 116 | resolve: (_root, args) => { 117 | return Decimal.add(args.a, args.b); 118 | }, 119 | }, 120 | }, 121 | }), 122 | }); 123 | expect( 124 | await graphql({ 125 | schema, 126 | source: /* GraphQL */ ` 127 | query ($a: Decimal, $b: Decimal) { 128 | sum(a: $a, b: $b) 129 | } 130 | `, 131 | variableValues: { 132 | a: new Decimal(0.1), 133 | b: '0.2', 134 | }, 135 | }), 136 | ).toEqual({ 137 | data: { 138 | sum: '0.3', 139 | }, 140 | }); 141 | }); 142 | 143 | it('null', async () => { 144 | const schema = new GraphQLSchema({ 145 | query: new GraphQLObjectType({ 146 | name: 'Query', 147 | fields: { 148 | decimal: { 149 | type: GraphQLDecimal, 150 | resolve: () => null, 151 | }, 152 | }, 153 | }), 154 | }); 155 | 156 | expect( 157 | await graphql({ 158 | schema, 159 | source: /* GraphQL */ ` 160 | query { 161 | decimal 162 | } 163 | `, 164 | }), 165 | ).toEqual({ 166 | data: { 167 | decimal: null, 168 | }, 169 | }); 170 | }); 171 | 172 | it('unknown value to parse', async () => { 173 | const schema = new GraphQLSchema({ 174 | query: new GraphQLObjectType({ 175 | name: 'Query', 176 | fields: { 177 | field: { 178 | type: GraphQLDecimal, 179 | args: { 180 | arg1: { type: GraphQLDecimal }, 181 | }, 182 | resolve: (_root, args) => { 183 | return args.arg1 === null ? 'failed to parse' : args.arg1; 184 | }, 185 | }, 186 | }, 187 | }), 188 | }); 189 | 190 | expect( 191 | await graphql({ 192 | schema, 193 | source: /* GraphQL */ ` 194 | query { 195 | field(arg1: false) 196 | } 197 | `, 198 | }), 199 | ).toEqual({ 200 | data: { 201 | field: 'failed to parse', 202 | }, 203 | }); 204 | }); 205 | 206 | describe('decimal create from object', () => { 207 | it('instanceof', () => { 208 | const decimal = new Decimal(0.123); 209 | // eslint-disable-next-line total-functions/no-unsafe-type-assertion 210 | const o = createDecimalFromObject({ 211 | d: decimal.d, 212 | e: decimal.e, 213 | s: decimal.s, 214 | }); 215 | 216 | expect(o).toBeInstanceOf(Decimal); 217 | expect(o instanceof Decimal).toBeTruthy(); 218 | }); 219 | 220 | for (const { decimal, string } of [ 221 | { decimal: new Decimal(0.123), string: '0.123' }, 222 | { decimal: new Decimal(1.234_567_89), string: '1.23456789' }, 223 | { decimal: new Decimal(4.6875e-2), string: '0.046875' }, 224 | { decimal: new Decimal('1.79e+308'), string: '1.79e+308' }, 225 | { decimal: new Decimal('9007199254741991'), string: '9007199254741991' }, 226 | ]) { 227 | it(`${decimal.toString()}`, () => { 228 | // eslint-disable-next-line total-functions/no-unsafe-type-assertion 229 | const o = Object.create(Decimal.prototype, { 230 | d: { value: decimal.d }, 231 | e: { value: decimal.e }, 232 | s: { value: decimal.s }, 233 | }) as Decimal; 234 | 235 | expect(o.toString()).toEqual(string); 236 | }); 237 | } 238 | }); 239 | 240 | describe('class transformer', () => { 241 | it('check type', () => { 242 | class Transfer { 243 | @Type(() => Object) 244 | @Transform(transformToDecimal) 245 | money!: Decimal; 246 | } 247 | }); 248 | 249 | it('transformToDecimal main case', () => { 250 | class Transfer { 251 | @Type(() => Object) 252 | @Transform(transformToDecimal) 253 | money!: Decimal; 254 | } 255 | 256 | const transfer = plainToClass(Transfer, { money: new Decimal(1) }); 257 | expect(transfer.money).toBeInstanceOf(Decimal); 258 | expect(transfer.money.isInteger()).toBe(true); 259 | }); 260 | 261 | it('transformToDecimal duck type', () => { 262 | class Transfer { 263 | @Type(() => Object) 264 | @Transform(transformToDecimal) 265 | money!: any; 266 | } 267 | 268 | const transfer = plainToClass(Transfer, { money: { toString: () => '1' } }); 269 | expect(transfer.money).toBeInstanceOf(Decimal); 270 | }); 271 | 272 | it('transformToDecimal array', () => { 273 | class Transfer { 274 | @Type(() => Object) 275 | @Transform(transformToDecimal) 276 | moneys!: Array; 277 | } 278 | 279 | const transfer = plainToClass(Transfer, { moneys: [new Decimal(1)] }); 280 | expect(transfer.moneys[0]).toBeInstanceOf(Decimal); 281 | }); 282 | 283 | it('array in nested object', () => { 284 | class Transfers { 285 | @Type(() => Object) 286 | @Transform(transformToDecimal) 287 | moneys!: Array; 288 | } 289 | class Container { 290 | @Type(() => Transfers) 291 | set!: Transfers; 292 | } 293 | 294 | const container = plainToClass(Container, { 295 | set: { moneys: [new Decimal(1)] }, 296 | }); 297 | expect(container.set.moneys[0]).toBeInstanceOf(Decimal); 298 | }); 299 | 300 | it('null should not be transformed', () => { 301 | class Transfer { 302 | @Type(() => Object) 303 | @Transform(transformToDecimal) 304 | money?: Decimal; 305 | } 306 | 307 | const transfer = plainToClass(Transfer, { money: null }); 308 | expect(transfer).toEqual({ money: null }); 309 | }); 310 | 311 | it('undefined should not be transformed', () => { 312 | class Transfer { 313 | @Type(() => Object) 314 | @Transform(transformToDecimal) 315 | money?: Decimal; 316 | } 317 | 318 | const transfer = plainToClass(Transfer, { money: undefined }); 319 | expect(transfer).toEqual({ money: undefined }); 320 | }); 321 | }); 322 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Decimal } from '@prisma/client/runtime/library.js'; 2 | import { GraphQLScalarType, GraphQLScalarTypeConfig, Kind } from 'graphql'; 3 | 4 | const config: GraphQLScalarTypeConfig< 5 | null | string | number | Decimal, 6 | string 7 | > = { 8 | name: 'Decimal', 9 | description: 'An arbitrary-precision Decimal type', 10 | /** 11 | * Value sent to the client 12 | */ 13 | serialize(value) { 14 | // console.log('serialize value', value.constructor.name); 15 | return String(value); 16 | }, 17 | /** 18 | * Value from the client 19 | */ 20 | parseValue(value) { 21 | return new Decimal(value as Decimal.Value); 22 | }, 23 | parseLiteral(ast) { 24 | if ( 25 | ast.kind === Kind.INT || 26 | ast.kind === Kind.FLOAT || 27 | ast.kind === Kind.STRING 28 | ) { 29 | return new Decimal(ast.value); 30 | } 31 | // eslint-disable-next-line unicorn/no-null 32 | return null; 33 | }, 34 | }; 35 | 36 | export const GraphQLDecimal = new GraphQLScalarType(config); 37 | 38 | export function createDecimalFromObject(object: any) { 39 | // eslint-disable-next-line total-functions/no-unsafe-type-assertion 40 | return Object.create(Decimal.prototype, { 41 | d: { value: object.d }, 42 | e: { value: object.e }, 43 | s: { value: object.s }, 44 | }) as Decimal; 45 | } 46 | 47 | interface TransformFunctionParams { 48 | value: any; 49 | } 50 | 51 | export function transformToDecimal({ value }: TransformFunctionParams) { 52 | if (value == null) return value; 53 | return Array.isArray(value) 54 | ? value.map(createDecimalFromObject) 55 | : createDecimalFromObject(value); 56 | } 57 | -------------------------------------------------------------------------------- /stryker.conf.mjs: -------------------------------------------------------------------------------- 1 | // Take a look at: https://stryker-mutator.io/docs/stryker-js/configuration/ for more information 2 | /** @type {import('@stryker-mutator/api/core').PartialStrykerOptions} */ 3 | export default { 4 | packageManager: 'npm', 5 | commandRunner: { 6 | command: 7 | 'node -r ts-node/register node_modules/mocha/bin/mocha src/**/*.spec.ts', 8 | }, 9 | coverageAnalysis: 'perTest', 10 | concurrency: 2, 11 | mutate: ['src/**/*.ts', '!src/**/*.spec.ts'], 12 | reporters: ['clear-text', 'dots'], 13 | tsconfigFile: 'tsconfig.json', 14 | }; 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "ts-node": { 3 | "swc": true, 4 | "transpileOnly": true, 5 | "moduleTypes": { 6 | "webpack.config.js": "cjs" 7 | }, 8 | "require": [] 9 | }, 10 | "compilerOptions": { 11 | "target": "ES2020", 12 | "module": "commonjs", 13 | "moduleResolution": "node", 14 | "importHelpers": true, 15 | "strict": true, 16 | "noUncheckedIndexedAccess": true, 17 | "noImplicitAny": false, 18 | "experimentalDecorators": true, 19 | "emitDecoratorMetadata": true, 20 | "outDir": "dist", 21 | "pretty": true, 22 | "esModuleInterop": true, 23 | "removeComments": false, 24 | "sourceMap": true, 25 | "declaration": true, 26 | "declarationMap": false, 27 | "skipLibCheck": true, 28 | "lib": ["esnext"], 29 | "paths": {}, 30 | "plugins": [] 31 | }, 32 | "include": ["src"] 33 | } 34 | --------------------------------------------------------------------------------