diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..d6ec728 Binary files /dev/null and b/.DS_Store differ diff --git a/.changeset/config.json b/.changeset/config.json index 6d2119a..cee6df8 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -4,7 +4,7 @@ "commit": false, "fixed": [], "linked": [], - "access": "restricted", + "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", "ignore": [] diff --git a/.changeset/fast-badgers-jog.md b/.changeset/fast-badgers-jog.md new file mode 100644 index 0000000..e03ee13 --- /dev/null +++ b/.changeset/fast-badgers-jog.md @@ -0,0 +1,5 @@ +--- +'@team-plain/typescript-sdk': patch +--- + +First public release 🎉 diff --git a/.github/workflows/generated-files.yml b/.github/workflows/generated-files.yml new file mode 100644 index 0000000..97c6565 --- /dev/null +++ b/.github/workflows/generated-files.yml @@ -0,0 +1,33 @@ +name: Generated files up-to-date +on: + push: + branches: + - main + pull_request: + +jobs: + generated-files: + name: Generated files up-to-date + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: '16' + - name: Install dependencies + run: npm install + - name: Run codegen + run: npm run codegen + - name: Check if there are any changes + id: changes + run: echo "changed=$(git status --porcelain | wc -l)" >> $GITHUB_OUTPUT + - name: "Failure: generated file changes detected" + if: steps.changes.outputs.changed > 0 + run: | + echo "Changes detected:" + git status --porcelain + for file in $(git status -s | cut -c4-) + do + echo "::error file=$file::$file not up to date" + done + exit 1 diff --git a/.gitignore b/.gitignore index c2dd9a5..9860320 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ node_modules dist -.vscode \ No newline at end of file +.vscode + +# These are generated and not commited +docs \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 773a3ca..38f6f89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,25 +1,2 @@ # @team-plain/typescript-sdk -## 0.0.7 - -### Patch Changes - -- 84ea15a: Updated readme - -## 0.0.6 - -### Patch Changes - -- 989ede5: Make package public - -## 0.0.5 - -### Patch Changes - -- 33b264a: Updated readme. - -## 0.0.4 - -### Patch Changes - -- 24abeea: Add support for upserting customers. diff --git a/README.md b/README.md index 0be6ae2..e2733a9 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,92 @@ -# typescript-sdk +# @team-plain/typescript-sdk -This is the Typescript/JS SDK for Plain.com's Core API. It allows you to do basic actions. +[Changelog]('./CHANGELOG.md') + +This is the typescript/node SDK for Plain.com's Core GraphQL API. It makes it easy to make common API calls in just a few lines of code. + +If you run into any issues please open an issue or get in touch with us at help@plain.com. + +## Basic example + +```ts +import { PlainSDKClient } from "@team-plain/typescript-sdk" + +const client = new PlainSDKClient({ + apiKey: 'plainApiKey__tmRD_xF5qiMH0657LkbLCC1maN4hLsBIbyOgjqEP4w' +}) + +const result = await client.getCustomerById({ customerId: 'c_01GHC4A88A9D49Q30AAWR3BN7P' }); + +if (result.error) { + console.log(result.error); +} else { + console.log(result.data.fullName); +} +``` + +You can find out how to make an API key in our documentation: https://docs.plain.com/core-api/authentication + + +## Documentation + +Every method in the SDK corresponds to a graphql [query](./src/graphql/queries/) or [mutation](./src/graphql/mutations/). + +You can find the generated documentation here: + +**[Documentation](https://plain-typescript-sdk-docs.vercel.app/classes/PlainSDKClient.html)** + +If you would like to add a query or mutation please open an issue and we can add it for you. + + +## Error handling +Every SDK method will return an object with either data or an error. + +**You will either receive an error or data, never both.** + +Here is an example: + +```ts +const client = new PlainSDKClient({ + apiKey: 'plainApiKey__tmRD_xF5qiMH0667LkbLCC1maN2hLsBIbyOgjqEP4w' +}) + +function doThing() { + const result = await client.getCustomerById({ customerId: 'c_01GHC4A88A9D49Q30AAWR3BN7P' }); + + if (result.error) { + console.log(result.error); + } else { + console.log(result.data.fullName); + } +} +``` + +An error can be **one of** the below: + +### MutationError +[(view source)](./src/error.ts) +This is the richest error type. It is called `MutationError` since it maps to the `MutationError` type in our GraphQL schema and is returned as part of every mutation in our API. + +You can view the full details of this error under `errorDetails`. + +Every mutation error will contain: +- **message**: an English technical description of the error. This error is usually meant to be read by a developer and not an end user. +- **type**: one of `VALIDATION`, `FORBIDDEN`, `INTERNAL`. See [MutationErrorType](https://docs.plain.com/core-api/reference/enums/mutation-error-type) for a description of each value. +- **code**: a unique error code for each type of error returned. This code can be used to provide a localized or user-friendly error message. You can find the list of error codes [in our docs](https://docs.plain.com/error-codes) . +- **fields**: an array containing all the fields that errored. Each field: + - **field**: the name of the input field the error is for + - **message**: an English technical description of the error. This error is usually meant to be read by a developer and not an end user. +type: one of `VALIDATION`, `REQUIRED`, `NOT_FOUND`. See [MutationFieldErrorType](https://docs.plain.com/core-api/reference/enums/mutation-field-error-type) for a description of each value. + +### BadRequestError +[(view source)](./src/error.ts) +Equivalent to a 400 response. If you are using typescript it's unlikely you will run into this since types will prevent this but if you are using javascript this likely means you are providing a wrong input/argument to a query or mutation. + +### ForbiddenError +[(view source)](./src/error.ts) +Equivalent to a 401 or 403 response. Normally means your API key doesn't exist or that you are trying to query something that you do not have permissions for. + +### InternalServerError +[(view source)](./src/error.ts) +Equivalent to a 500 response. If this happens something unexpected within Plain happened. -This is a work in progress. Docs are coming soon! diff --git a/package-lock.json b/package-lock.json index aff9a05..b6df70d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@team-plain/typescript-sdk", - "version": "0.0.3", + "version": "0.0.7", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@team-plain/typescript-sdk", - "version": "0.0.3", + "version": "0.0.7", "license": "MIT", "dependencies": { "@graphql-typed-document-node/core": "^3.2.0", @@ -30,6 +30,8 @@ "rollup": "^3.21.5", "rollup-plugin-dts": "^5.3.0", "rollup-plugin-esbuild": "^5.0.0", + "typedoc": "^0.24.7", + "typedoc-plugin-missing-exports": "^2.0.0", "vitest": "^0.31.0" } }, @@ -3526,6 +3528,12 @@ "node": ">=8" } }, + "node_modules/ansi-sequence-parser": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.0.tgz", + "integrity": "sha512-lEm8mt52to2fT8GhciPCGeCXACSz2UwIN4X2e2LJSnZ5uAbn2/dsYdOmUXq0AtWS5cpAupysIneExOgH0Vd2TQ==", + "dev": true + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -6715,6 +6723,12 @@ "yallist": "^3.0.2" } }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true + }, "node_modules/magic-string": { "version": "0.30.0", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz", @@ -6748,6 +6762,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "dev": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/md5-hex": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/md5-hex/-/md5-hex-3.0.1.tgz", @@ -8190,6 +8216,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/shiki": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.2.tgz", + "integrity": "sha512-ltSZlSLOuSY0M0Y75KA+ieRaZ0Trf5Wl3gutE7jzLuIcWxLp5i/uEnLoQWNvgKXQ5OMpGkJnVMRLAuzjc0LJ2A==", + "dev": true, + "dependencies": { + "ansi-sequence-parser": "^1.1.0", + "jsonc-parser": "^3.2.0", + "vscode-oniguruma": "^1.7.0", + "vscode-textmate": "^8.0.0" + } + }, "node_modules/side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -8876,6 +8914,60 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typedoc": { + "version": "0.24.7", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.24.7.tgz", + "integrity": "sha512-zzfKDFIZADA+XRIp2rMzLe9xZ6pt12yQOhCr7cD7/PBTjhPmMyMvGrkZ2lPNJitg3Hj1SeiYFNzCsSDrlpxpKw==", + "dev": true, + "dependencies": { + "lunr": "^2.3.9", + "marked": "^4.3.0", + "minimatch": "^9.0.0", + "shiki": "^0.14.1" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 14.14" + }, + "peerDependencies": { + "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x" + } + }, + "node_modules/typedoc-plugin-missing-exports": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/typedoc-plugin-missing-exports/-/typedoc-plugin-missing-exports-2.0.0.tgz", + "integrity": "sha512-t0QlKCm27/8DaheJkLo/gInSNjzBXgSciGhoLpL6sLyXZibm7SuwJtHvg4qXI2IjJfFBgW9mJvvszpoxMyB0TA==", + "dev": true, + "peerDependencies": { + "typedoc": "0.24.x" + } + }, + "node_modules/typedoc/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/typedoc/node_modules/minimatch": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.0.tgz", + "integrity": "sha512-0jJj8AvgKqWN05mrwuqi8QYKx1WmYSUoKSxu5Qhs9prezTz10sxAHGNZe9J9cqIJzta8DWsleh2KaVaLl6Ru2w==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/typescript": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", @@ -9206,6 +9298,18 @@ } } }, + "node_modules/vscode-oniguruma": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", + "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==", + "dev": true + }, + "node_modules/vscode-textmate": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz", + "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==", + "dev": true + }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", @@ -12067,6 +12171,12 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, + "ansi-sequence-parser": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.0.tgz", + "integrity": "sha512-lEm8mt52to2fT8GhciPCGeCXACSz2UwIN4X2e2LJSnZ5uAbn2/dsYdOmUXq0AtWS5cpAupysIneExOgH0Vd2TQ==", + "dev": true + }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -14428,6 +14538,12 @@ "yallist": "^3.0.2" } }, + "lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true + }, "magic-string": { "version": "0.30.0", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz", @@ -14449,6 +14565,12 @@ "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", "dev": true }, + "marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "dev": true + }, "md5-hex": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/md5-hex/-/md5-hex-3.0.1.tgz", @@ -15493,6 +15615,18 @@ "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", "dev": true }, + "shiki": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.2.tgz", + "integrity": "sha512-ltSZlSLOuSY0M0Y75KA+ieRaZ0Trf5Wl3gutE7jzLuIcWxLp5i/uEnLoQWNvgKXQ5OMpGkJnVMRLAuzjc0LJ2A==", + "dev": true, + "requires": { + "ansi-sequence-parser": "^1.1.0", + "jsonc-parser": "^3.2.0", + "vscode-oniguruma": "^1.7.0", + "vscode-textmate": "^8.0.0" + } + }, "side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -16050,6 +16184,45 @@ "is-typed-array": "^1.1.9" } }, + "typedoc": { + "version": "0.24.7", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.24.7.tgz", + "integrity": "sha512-zzfKDFIZADA+XRIp2rMzLe9xZ6pt12yQOhCr7cD7/PBTjhPmMyMvGrkZ2lPNJitg3Hj1SeiYFNzCsSDrlpxpKw==", + "dev": true, + "requires": { + "lunr": "^2.3.9", + "marked": "^4.3.0", + "minimatch": "^9.0.0", + "shiki": "^0.14.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.0.tgz", + "integrity": "sha512-0jJj8AvgKqWN05mrwuqi8QYKx1WmYSUoKSxu5Qhs9prezTz10sxAHGNZe9J9cqIJzta8DWsleh2KaVaLl6Ru2w==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "typedoc-plugin-missing-exports": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/typedoc-plugin-missing-exports/-/typedoc-plugin-missing-exports-2.0.0.tgz", + "integrity": "sha512-t0QlKCm27/8DaheJkLo/gInSNjzBXgSciGhoLpL6sLyXZibm7SuwJtHvg4qXI2IjJfFBgW9mJvvszpoxMyB0TA==", + "dev": true, + "requires": {} + }, "typescript": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", @@ -16234,6 +16407,18 @@ "why-is-node-running": "^2.2.2" } }, + "vscode-oniguruma": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", + "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==", + "dev": true + }, + "vscode-textmate": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz", + "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==", + "dev": true + }, "wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", diff --git a/package.json b/package.json index 8752b58..cedd2be 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "typings": "dist/index.d.ts", "scripts": { "build": "rm -rf dist && rollup -c", + "build:docs": "typedoc --plugin typedoc-plugin-missing-exports src/index.ts", "codegen": "graphql-codegen", "typecheck": "tsc --noEmit", "lint": "eslint 'src/**/*.ts'", @@ -31,6 +32,8 @@ "rollup": "^3.21.5", "rollup-plugin-dts": "^5.3.0", "rollup-plugin-esbuild": "^5.0.0", + "typedoc": "^0.24.7", + "typedoc-plugin-missing-exports": "^2.0.0", "vitest": "^0.31.0" }, "dependencies": { diff --git a/src/client.ts b/src/client.ts index dc0dc94..68c0378 100644 --- a/src/client.ts +++ b/src/client.ts @@ -41,12 +41,15 @@ function unwrapData( export class PlainSDKClient { #ctx: Context; - constructor(props: { apiKey: string }) { + constructor(options: { apiKey: string }) { this.#ctx = { - apiKey: props.apiKey, + apiKey: options.apiKey, }; } + /** + * If you need to do something custom you can use this method to do + */ async rawRequest(args: { query: string; variables: Record; @@ -57,6 +60,9 @@ export class PlainSDKClient { }); } + /** + * If the customer is not found this will return null. + */ async getCustomerById(args: CustomerByIdQueryVariables): SDKResult { const res = await request(this.#ctx, { query: CustomerByIdDocument, @@ -68,6 +74,10 @@ export class PlainSDKClient { return unwrapData(res, (q) => q.customer); } + /** + * Allows you to create or update a customer. If you need to get the customer id + * for a customer in Plain, this is typically your first step. + */ async upsertCustomer(input: UpsertCustomerInput): SDKResult { const res = await request(this.#ctx, { query: UpsertCustomerDocument, @@ -79,6 +89,10 @@ export class PlainSDKClient { return unwrapData(res, (q) => nonNullable(q.upsertCustomer.customer)); } + /** + * Create an issue for a customer. If you want you can override the default issue priority + * in your settings by specifying a priority manually here. + */ async createIssue(input: CreateIssueInput): SDKResult { const res = await request(this.#ctx, { query: CreateIssueDocument, diff --git a/src/error.ts b/src/error.ts index a7f7748..01979f4 100644 --- a/src/error.ts +++ b/src/error.ts @@ -3,33 +3,33 @@ import { MutationErrorPartsFragment } from './graphql/types'; /* 400 */ type BadRequestError = { - code: 'bad_request'; + type: 'bad_request'; message: string; graphqlErrors: PlainGraphQLError[]; }; /* 401 */ type ForbiddenError = { - code: 'forbidden'; + type: 'forbidden'; message: string; }; /* 500 */ type InternalServerError = { - code: 'internal_server_error'; + type: 'internal_server_error'; message: string; }; /* Unhandled/unexpected errors */ type UnknownError = { - code: 'unknown'; + type: 'unknown'; message: string; err?: unknown; }; /* Handled mutation errors */ type MutationError = { - code: 'mutation_error'; + type: 'mutation_error'; message: string; errorDetails: MutationErrorPartsFragment; }; diff --git a/src/graphql/queries/customer.gql b/src/graphql/queries/customerById.gql similarity index 100% rename from src/graphql/queries/customer.gql rename to src/graphql/queries/customerById.gql diff --git a/src/graphql/types.ts b/src/graphql/types.ts index 2629fb0..26e006f 100644 --- a/src/graphql/types.ts +++ b/src/graphql/types.ts @@ -2424,10 +2424,13 @@ export type Query = { myLinearInstallationInfo: UserLinearInstallationInfo; myLinearIntegration: Maybe; myLinearIntegrationToken: Maybe; + myMachineUser: Maybe; + myPermissions: Permissions; mySlackInstallationInfo: UserSlackInstallationInfo; mySlackIntegration: Maybe; myUser: Maybe; myUserAccount: Maybe; + myWorkspace: Maybe; myWorkspaceInvites: WorkspaceInviteConnection; myWorkspaces: WorkspaceConnection; permissions: Permissions; diff --git a/src/request.ts b/src/request.ts index b73e508..436ad5f 100644 --- a/src/request.ts +++ b/src/request.ts @@ -40,9 +40,18 @@ export async function request( const mutationError = getMutationErrorFromResponse(res.data); if (mutationError) { + if (mutationError.code === 'forbidden') { + return { + error: { + type: 'forbidden', + message: mutationError.message, + }, + }; + } + return { error: { - code: 'mutation_error', + type: 'mutation_error', message: mutationError.message, errorDetails: mutationError, }, @@ -56,10 +65,10 @@ export async function request( if (axios.isAxiosError(err)) { // Case 1: We got a response back that was > 299 in status code if (err.response) { - if (err.response.status === 401) { + if (err.response.status === 401 || err.response.status === 403) { return { error: { - code: 'forbidden', + type: 'forbidden', message: 'Authentication failed. Please check the provided API key.', }, }; @@ -68,7 +77,7 @@ export async function request( if (err.response.status === 400 && isPlainGraphQLResponse(err.response.data)) { return { error: { - code: 'bad_request', + type: 'bad_request', message: 'Missing or invalid arguments provided.', graphqlErrors: err.response.data.errors || [], }, @@ -78,7 +87,7 @@ export async function request( if (err.response.status === 500) { return { error: { - code: 'internal_server_error', + type: 'internal_server_error', message: 'Internal server error.', }, }; @@ -88,7 +97,7 @@ export async function request( if (err.request) { return { error: { - code: 'unknown', + type: 'unknown', message: err.message, }, }; @@ -98,7 +107,7 @@ export async function request( // Case 3: Something completely unhandled happened return { error: { - code: 'unknown', + type: 'unknown', message: 'Unknown error', err, }, diff --git a/src/tests/error-handling.test.ts b/src/tests/error-handling.test.ts index c240ace..5f26c44 100644 --- a/src/tests/error-handling.test.ts +++ b/src/tests/error-handling.test.ts @@ -17,7 +17,7 @@ describe('error handling', () => { const result = await client.getCustomerById({ customerId: 'c_123' }); expect(result.error).toEqual({ - code: 'internal_server_error', + type: 'internal_server_error', message: 'Internal server error.', }); @@ -25,17 +25,60 @@ describe('error handling', () => { }); test('should return a forbidden error when API 401s', async () => { - const scope = interceptor.matchHeader('Authorization', 'Bearer 123').reply(401, 'forbidden'); + const scope = interceptor.matchHeader('Authorization', 'Bearer 123').reply(401, 'unauthorized'); const client = new PlainSDKClient({ apiKey: '123' }); const result = await client.getCustomerById({ customerId: 'c_123' }); expect(result.error).toEqual({ - code: 'forbidden', + type: 'forbidden', message: expect.stringContaining('Authentication failed'), }); scope.done(); }); + + test('should return a forbidden error when API 403s', async () => { + const scope = interceptor.matchHeader('Authorization', 'Bearer 123').reply(403, 'forbidden'); + + const client = new PlainSDKClient({ apiKey: '123' }); + + const result = await client.getCustomerById({ customerId: 'c_123' }); + + expect(result.error).toEqual({ + type: 'forbidden', + message: expect.stringContaining('Authentication failed'), + }); + + scope.done(); + }); + + test('should return a forbidden error when API responds with mutation error', async () => { + const scope = interceptor.matchHeader('Authorization', 'Bearer 123').reply(200, { + data: { + updateCustomerGroup: { + customerGroup: null, + error: { + __typename: 'MutationError', + message: 'Insufficient permissions, missing "customerGroup:edit".', + type: 'FORBIDDEN', + code: 'forbidden', + fields: [], + }, + }, + }, + }); + + const client = new PlainSDKClient({ apiKey: '123' }); + + const result = await client.getCustomerById({ customerId: 'c_123' }); + + expect(result.error).toEqual({ + type: 'forbidden', + message: 'Insufficient permissions, missing "customerGroup:edit".', + }); + + scope.done(); + }); }); diff --git a/src/tests/mutation.test.ts b/src/tests/mutation.test.ts index ec99987..351fab8 100644 --- a/src/tests/mutation.test.ts +++ b/src/tests/mutation.test.ts @@ -118,7 +118,7 @@ describe('mutation test - create an issue', () => { const result = await client.createIssue({ customerId: '', issueTypeId: '', priorityValue: 1 }); const err: PlainSDKError = { - code: 'mutation_error', + type: 'mutation_error', message: 'There was a validation error.', errorDetails: graphqlError, }; diff --git a/src/tests/query.test.ts b/src/tests/query.test.ts index 1085fc1..6c4ef73 100644 --- a/src/tests/query.test.ts +++ b/src/tests/query.test.ts @@ -102,7 +102,7 @@ describe('query test - customer by id', () => { const result = await client.getCustomerById({} as any); const err: PlainSDKError = { - code: 'bad_request', + type: 'bad_request', message: 'Missing or invalid arguments provided.', graphqlErrors, }; diff --git a/src/tests/raw-request.test.ts b/src/tests/raw-request.test.ts index 1cf7b70..90e765e 100644 --- a/src/tests/raw-request.test.ts +++ b/src/tests/raw-request.test.ts @@ -66,7 +66,7 @@ describe('raw request', () => { }); const err: PlainSDKError = { - code: 'bad_request', + type: 'bad_request', message: 'Missing or invalid arguments provided.', graphqlErrors, };